fwd-router — FWD source routing and the /net plane (L4)¶
In one paragraph
fwd_router_t is the node’s hop-by-hop FWD forwarder and the whole of its remote-operation
surface. It binds one local graph_t to a set of named transport children held in a
child_registry_t, and on each inbound frame it does exactly one of three things: strip the
first dst segment and forward, resolve a local terminus and reply, or hand a fully-consumed
REPLY to the reply sink. It is stateless per request — the forward route is the shrinking
dst and the return route is the growing src, both carried in the frame — so a hop may reboot
mid-operation and the reply routes regardless. Connections themselves are vertices under /net
(transport_vertex_t), created in band and addressed at /net/<module>/<name>.
What it does¶
The net plane is explicit-source-routed FWD only
(ADR-0040 — the net plane is explicit-source-routed only).
A remote endpoint is addressed by its full path through transport vertices; each hop consumes the
segments that name its own link, so dst shrinks toward the target while src accumulates the way
back. That makes a route loop-free by construction and needs no per-hop dedup state: there
is no flooding, and no (origin, ts) suppression table. Two parallel links to the same peer are
therefore two different explicit addresses — deliberate redundancy chosen by the addresser, not
auto-multipath discovered by the router. 0x0D ROUTER is a reserved, decodable wire code
(type_t::ROUTER, core/include/libtracer/tlv.hpp:type_t::ROUTER) with no implemented mechanism behind it;
source routing needs none.
Four dispositions. Three are decided by resolving the first dst segment against the registry; the fourth is decided by the dst’s own type code, because a bound address has no segment to resolve:
First |
Disposition |
|---|---|
a registered transport child |
forward — strip the segment from |
a local non-transport vertex |
terminus — decode into an arena, apply the op, build |
a |
bound hop — consume element 0 (bounds, generation, ACL at the dereferenced vertex), egress the residual over the link it names (§bound hop) |
nothing, on a |
terminal reply — the accumulated return route is fully consumed, so this node is the originator; the frame goes to the reply sink |
A REPLY routes by the same step but never accumulates src: a reply expects no reply
(RFC-0004 — remote operation addressing §B).
The forward hop builds no decoded tree at all. It reads the frame’s headers by offset, writes
the shortened dst and grown src heads into small stack buffers, and hands the transport those
heads plus untouched views of the inbound frame. Only a terminus request decodes, and it decodes
into an arena drawn from a failable block source rather than the container resource — so a
peer-sized frame arriving behind no ACL cannot exhaust the heap silently. Registry lookups take no
lock; the control-plane mutex covers add_child / remove_child only, and the forward path never
takes it.
The bound hop¶
A dst that is a PATH_REF rather than a canonical PATH takes a fourth disposition, and it is
decided by the element count rather than by the registry (RFC-0024 — bound paths §3.4/§5):
one element left means this node is the terminus and the element names its target vertex;
more than one means this node is a hop, and it consumes element 0 — bounds-check the index,
compare the generation, evaluate the ACL at the dereferenced vertex for the operation’s own right
— then egresses the residual over the link that vertex names. No mount descent runs at all: the
resolve_mount_* family is not entered, there is no digest fold and no segment compare.
One peek decides all four dispositions. The dst’s form — canonical PATH, bound PATH_REF, or
neither — is read once, from the three headers a frame leads with, and the two forms are mutually
exclusive by that type code. Asking the canonical question and the bound question as separate
walks costs the bound form a whole second parse of the same bytes while buying the canonical form
nothing, and it measured a bound terminus slower than the canonical terminus it exists to beat.
The element→link join is one integer per child, recorded at add_child: a child’s mount run is
its connection vertex’s canonical key, so the router resolves that vertex once and remembers its
slot index. It is not a route table — one entry per link, sized by the graph and never by the
traffic — and a child registered before its connection vertex exists simply has none, which makes
every bound route through it fall back to canonical rather than misroute. A bus child never
records one, deliberately: a bus mount’s own send() broadcasts and a bus peer has no vertex, so
no element can name either.
An opcode the build cannot name is dropped rather than forwarded: §6.2 evaluates the ACL for the
operation’s own right, and a hop that does not know an opcode does not know its right, so
charging it the READ right that happens to be at hand is a guess a future write-like opcode
would cross a read-only gate on. A bound REPLY is refused the same way.
Any validation failure is a drop, and never a fall-through to the local terminus: a bound frame
this node cannot route is dropped, the origin still holds the canonical path the binding was minted
from, and re-resolving canonically and re-minting is its recovery. src accumulates canonically
throughout, so the reply of a bound request routes home through the ordinary descent and every hop
on the way back may be a peer that does not speak the bound form at all.
A hop that forwards a mint reply either contributes its element or strips the answer. Every cannot-contribute case strips — no connection vertex for the inbound link, a saturated or retired generation, and a list already at the 255-element cap — because a relayed list that skips a hop is not a shorter route but a wrong one: the skipped hop would later find one element left, believe itself the terminus, and dereference another host’s element against its own vertex map.
The router also carries the origin’s half — connection_ref, bound_egress, adopt_binding and
bound_dispatch — because both halves are the same act: consume element 0, dereference it,
egress. The origin’s element is the one no peer can supply, since the hop out of this node is the
one hop nobody else sees.
The path-label origin (RFC-0027 §6.1 amendment 8) is the same shape one plane over:
adopt_path_label caches the spelling a minted reply’s src came back with, label_dispatch
spends it as the next request’s dst, and fall_back_on_label_refusal drops it on
tr::path::not_found. What differs is what the origin contributes for its own first hop. A bound
route needs an element only this node can mint, so the origin mints one; a labelled route does not
— the origin stands up no label table, prepends its own local part as literal segments, and
therefore holds a spelling that is mixed by construction. Consuming that head is the ordinary
strip-K mount descent, not a dereference, which is why this dispatch takes no ACL right: the
authorization of a labelled operation is decided at each hop that dereferences a label (§8.2).
Alongside the routing legs the router installs the graph’s remote-delivery sink: a write to a
vertex that carries a remote subscriber fans out as FWD{WRITE} addressed by that subscriber’s
stored return route, or — when the subscription is compact-flagged — as a lean COMPACT bearing a
label bound by a prior ADVERTISE.
Interface¶
namespace tr::net {
class fwd_router_t {
// graph: terminus op resolution. label_src: the NOTHROW block source the route-handle
// label tables draw from (#603 defect 1 / ADR-0065 — it was a `memory_resource` until a
// peer's ADVERTISE proved pmr cannot report exhaustion by value).
// rx: the NOTHROW block source the terminus decode arena draws from (ADR-0065).
// flat: the byte backend EVERY rope flatten draws from, forward AND terminus (#730/#766).
// max_label_bindings_per_link: 0 = unbounded. egress: the reply-egress backend (#795).
// FOUR independent memory seams, each defaulting to the global heap on its own.
explicit fwd_router_t(graph::graph_t& graph,
mem::block_source_t* label_src = &mem::heap_source(),
mem::block_source_t* rx = &mem::heap_source(),
mem::mem_backend_t* flat = &mem::heap_backend(),
std::size_t max_label_bindings_per_link = 0,
mem::mem_backend_t* egress = &mem::heap_backend());
// `name` is this node's mount RUN for `link`: the leading dst segments that route onward
// through it, and the run prepended to src on the way back. ANY width (#523) -- the
// descent makes one registry pass and matches each slot against the prefix of its own
// width, so the only bound is the path-depth budget. Returns false, always (not only in
// debug), for a name no address could ever name: empty, with an empty segment, or wider
// than kMaxSegments. Optional per-child failable source; null falls back to the router's.
bool add_child(std::string name, transport_t& link, mem::block_source_t* rx = nullptr);
bool remove_child(std::string_view name); // removal, not departure
void link_down(std::string_view link_name); // departure: evict edges + drop label state
// Bind a LOCAL producer's subscription toward a MOUNT-PATH target (#739): resolves
// `target` through the SAME strip-K cached descent the forward path uses (ADR-0061),
// derives (link, return route), and admits through graph_t::subscribe_wire — so a
// caller never hand-splits, and an arbitrarily nested /net/A/net/B/x target works.
// Bind-time resolution, link-lifetime durability: teardown drops the binding, and
// re-binding is the application's job. A bus-PEER first hop answers INVALID_PATH.
graph::result_t<void> subscribe_toward(const graph::path_t& producer,
const graph::path_t& target);
// Per-frame sinks: function pointer + opaque context, never std::function (ADR-0047).
using reply_fn_t = void (*)(void* ctx, const view::rope_t& reply);
using inbound_fn_t = void (*)(void* ctx, std::string_view inbound,
const wire::tlv_node_t& fwd); // read in place
using raw_fn_t = void (*)(void* ctx, std::string_view inbound,
std::span<const std::byte> frame);
using compact_delivery_fn_t = void (*)(void* ctx, std::span<const std::byte> route,
std::span<const std::byte> payload);
using stale_label_fn_t = void (*)(void* ctx, std::string_view inbound,
std::uint16_t label);
void on_reply(reply_fn_t, void* ctx = nullptr) noexcept;
void on_inbound(inbound_fn_t, void* ctx = nullptr) noexcept;
void on_raw(raw_fn_t, void* ctx = nullptr) noexcept;
void on_compact_delivery(compact_delivery_fn_t, void* ctx = nullptr) noexcept;
void on_stale_label(stale_label_fn_t, void* ctx = nullptr) noexcept;
// Route-handle producer side (RFC-0004 §E.1).
std::uint16_t advertise(std::string_view link_name, std::span<const std::byte> route_path);
void send_compact(std::string_view link_name, std::uint16_t label,
std::span<const std::byte> payload);
void clear_link(std::string_view link_name);
void on_frame(std::string_view inbound_name, std::span<const std::byte> frame);
// Originate a READ or WRITE (an append is a WRITE to `:field[]`) from inside this node,
// through the same ingress (#1645). The reply goes to the caller-owned record; the
// caller's deadline ends it with cancel(). See "Originating an operation" below.
class origin_t { public: origin_t(reply_fn_t, void* ctx) noexcept; bool armed() const noexcept; };
graph::result_t<void> originate(origin_t& slot, graph::fwd_op_t op, const graph::path_t& dst,
std::span<const std::byte> payload = {},
const graph::path_t* reply_to = nullptr);
bool cancel(origin_t& slot) noexcept;
const child_registry_t& registry() const noexcept;
const route_handle_t& handles() const noexcept;
};
class child_registry_t { // the one NAME -> link demux table (ADR-0037)
// The link and its SHAPE are ONE atomic word (#882): read as a pair or a rebind that
// flips a name's shape hands a forward one publication's shape with another's link.
struct egress_t { transport_t* link; bool multi_peer; }; // tombstone = null link
struct child_t { std::string name; std::uint64_t name_digest;
std::vector<std::byte> mount_tlv;
egress_t egress() const noexcept; transport_t* link() const noexcept;
bool live() const noexcept; };
bool add(std::string name, transport_t& link); // rebinds; false = no slot
bool erase(std::string_view name); // tombstones in place
// ONE pass, each slot matched against the prefix of its OWN seg_count (#523) — so a
// mount of any width resolves and there is no per-width retry. Longest match wins.
template <class SegAt> const child_t* longest_prefix(SegAt&& at) const; // the demux
transport_t* by_name(std::string_view name) const;
static transport_t* resolve_peer(const child_t&, std::string_view peer);
std::size_t size() const noexcept; std::size_t live_size() const noexcept;
};
} // namespace tr::net
Signature source: core/include/libtracer/fwd_router.hpp:fwd_router_t::fwd_router_t(graph::graph_t& graph (constructor; its planes are the router_planes_t aggregate at core/include/libtracer/fwd_router.hpp:router_planes_t), core/include/libtracer/fwd_router.hpp:fwd_router_t::add_child
(add_child), core/include/libtracer/fwd_router.hpp:fwd_router_t::subscribe_toward (subscribe_toward), core/include/libtracer/fwd_router.hpp:fwd_router_t::reply_fn_t (the sink function-pointer types);
core/include/libtracer/child_registry.hpp:child_registry_t::add (add), core/include/libtracer/child_registry.hpp:child_registry_t::resolve_peer (resolve_peer), core/include/libtracer/child_registry.hpp:child_registry_t::erase
(erase), core/include/libtracer/child_registry.hpp:child_registry_t::entry_by_name (entry_by_name), core/include/libtracer/child_registry.hpp:child_registry_t::by_name (by_name), core/include/libtracer/child_registry.hpp:child_registry_t::size/core/include/libtracer/child_registry.hpp:child_registry_t::live_size (size/live_size).
Routing one inbound frame¶
flowchart TB
IN["inbound frame on child NAME"] --> RAW["on_raw observer"]
RAW --> PEEK["offset peek: first dst segment"]
PEEK --> DEMUX{"child_registry_t<br/>longest_prefix"}
DEMUX -->|"resolves to a link"| FWD["strip dst segment<br/>prepend inbound NAME to src"]
FWD --> SG["stack-built heads<br/>+ untouched frame views"]
SG -->|"send(iov)"| OUT(["transport_t"])
DEMUX -->|"no link, op != REPLY"| TERM["arena decode<br/>(nothrow block source)"]
TERM --> RES["op_resolver_t against graph_t"]
RES --> RPL["FWD{REPLY} back over the inbound link"]
DEMUX -->|"no link, op == REPLY"| SINK["reply sink<br/>rope-native, no flatten"]
classDef zc fill:#dcfce7,stroke:#166534
class SG,SINK zc
Originating an operation¶
A node that wants a remote value asks for it with originate, not by building FWD bytes and
feeding them to on_frame under a child name nothing registered (#1645). Three things make it
the same operation a peer’s frame is:
One ingress.
originatebuildsFWD{ op, dst, FIELD?, src, payload? }into one exactly-sized segment from theegressplane and routes it through the path an inbound frame takes, with no inbound link. A forward hop strips the mount and growssrcby nothing, because the origin is not a hop. Adstthat names no mount resolves locally, and its reply goes straight to the caller’s record beforeoriginatereturns.The requester owns the request. The record (
origin_t) is the caller’s storage. The router threads armed records through an intrusive list and keeps nothing once one leaves, so the hops stay stateless and the router allocates nothing per request. No timer and no clock are involved: the caller decides how long to wait and callscancelwhen its deadline passes. A reply that arrives after that goes to theon_replysink, the same place an unpaired reply has always gone.Pairing by return route. By default each request gets its own one-record
src, a reserved~o<hex>name that no mount begins with, so the reply’sdstnames exactly one record. A subscribe needs a chosensrc, because the producer delivers to it, soreply_tosupplies one. Requests that share a route pair oldest first.
got_t got; // the caller's own reply state
fwd_router_t::origin_t slot{&on_got, &got}; // on_got(void*, const rope_t&)
if (auto r = router.originate(slot, fwd_op_t::READ, path_t("/net/ws-client/up/sensor/temp")); !r)
return r; // refused: nothing was sent
// ... wait on the caller's own condition, with the caller's own deadline ...
if (timed_out && router.cancel(slot)) { /* no reply will reach on_got for this request */ }
const path_t probe("/probe"); // must outlive the armed record
(void)router.originate(slot, fwd_op_t::WRITE, // an append: `[]` in the dst field tail
path_t("/net/ws-client/up/sensor/temp:subscribers[]"), subscriber_tlv,
&probe);
AWAIT is not originated through this entry point.
Signature source: core/include/libtracer/fwd_router.hpp:fwd_router_t::originate
(originate), core/include/libtracer/fwd_router.hpp:fwd_router_t::origin_t (origin_t),
core/include/libtracer/fwd_router.hpp:fwd_router_t::cancel (cancel).
Consequences¶
Stateless hops. No per-request table means no timeout sweeper, no correlation map, and no memory that scales with concurrent operations. The cost is that the frame carries its own route: address size grows with hop count, which is what
ADVERTISE/COMPACTroute handles exist to amortise on a steady flow.A reply is delivered as a rope, never flattened by the router (
core/include/libtracer/fwd_router.hpp:fwd_router_t::compact_delivery_fn_t). A sink that wants contiguous bytes holdsconst view_t m = reply.materialize()and readsm.bytes(); a single-link reply — the common case — is returned zero-copy, no allocation and no copy, and only a multi-link reply pays one flatten, on demand. The escape hatch sits at the consumer, so the router never pays for a consumer that did not need contiguity.mmust stay alive while its span is read.The default delivery leg copies nothing. A full-route
FWD{WRITE}fan-out scatter-gathers a fresh stack head, the stored return-route bytes, an emptysrc, and one span per link of the stored value (core/src/fwd_router.cpp:fwd_router_t::deliver_remote). TheCOMPACTleg is the one that flattens, because aCOMPACTwraps a contiguous payload (core/src/fwd_router.cpp:fwd_router_t::deliver_local) — single-link, that flatten is a zero-copy adopt, and multi-link it draws from the router’s injectedflatbackend (#730), not the global heap.All rope flattens on the forward AND terminus paths draw from the injected seam.
flatstarted (#730) as the router’s own four sites — the two ingress control-frame sub-rope flattens, the cold bus-name rejection flatten, and the per-deliveryCOMPACTegress one. The terminus half was not covered: the resolver’s rope-tier flattens, one call belowresolve_terminus_rope, tookrope_t::materialize’s default global-heap backend, so a fragmented request from a peer allocated outside a bounded node’s slab no matter what it had injected. The router now passes the same pointer to itsop_resolver_t(#766), which threads it through the rope-tier node reader, so one injection covers both paths. A refused terminus flatten is answered by value: an addressedkind=ERROR STATUS{BACKPRESSURE}reply, or — when the refusal hit the reply’s own route bytes, leaving no trustworthy address — a drop. Never a reply built on a short span. Since #793 it also covers the rope tier’s single-link ownership copy, which used to reach the global heap throughview::over_bytes— soown_wire’s two branches no longer allocate from two different allocators depending on where the peer’s fragmentation fell. Since #801 it covers the span (arena) tier’sown_wireas well — that tier’s only allocating site, and the one a span-delivering child (a synchronous CAN/UART link) takes on every ordinary WRITE — so which allocator a stored value’s bytes come from no longer depends on which transport delivered the frame. Whatflatstill does not cover: the terminus arena (that isrx’s nothrowblock_source_t, bounded by a different seam, not unbounded) and the reply head segment (genuinely unbounded on both tiers; it answers exhaustion by value, and folding it intoflatwould silently re-scope an injection callers have already sized — see failable allocation and backpressure).Delivery drops rather than aborts on the value path — with one named residual. The flatten, frame build and
iovecreserve on the writer thread are all failable: each drops that one delivery — a subscriber misses a value under exhaustion, which is valid delivery behaviour — instead of raising an exception that-fno-exceptionswould turn intoabort(). A dropped freshADVERTISEself-heals through the peer’sHANDLE_NACK. The label store used to be the residual here — a compact-flagged flow’s first delivery on a link resolves its label before those three steps, and that allocated itslink_tables_tand its egress entry from a throwingstd::pmr::memory_resource, so that one leg could abort under-fno-exceptions(#603 defect 1). It is closed: the store draws every byte from an injectedmem::block_source_tand answers exhaustion by value, so the leg degrades to the full-routeFWD{WRITE}form instead of aborting. See failable allocation and backpressure.Per-frame sinks are a function pointer plus a context, not
std::function(ADR-0047 — build-time closed module sets and compile-time seams). These fire on the per-frame receive path, where type-erasure machinery — code size, a heap capability, exception paths — is the largest avoidable embedded liability. It is the same call shapegraph_t’ssubscriber_fn_tmakes at L4. Passingnullptrclears a sink.A per-child block source is a pointer branch, not a lookup. Each transport owns its receive thread, so a source parked on that child’s receiver context is touched by exactly one thread — the per-thread shape obtained by ownership rather than by a lock — and a bounded node giving each child its own slab makes the bound per-peer, so one noisy link cannot starve another’s decode (ADR-0067 — bounded recycling source and per-owner topology §3).
The /net connection model¶
A connection is a vertex, not a hidden table entry (ADR-0027 — transports and connections are vertices), so it is addressable, readable, subscribable and removable by the same operations as any other vertex.
Creation is an ordinary write to the module’s creator endpoint. A SPEC written to
/net/<module>/conn — write /net/<module>/conn <- SPEC{name, config} — instantiates a
connection at /net/<module>/<name>, and a NAME{<name>} to the same endpoint removes it. The
SPEC’s config carries the universal keys kind, addr, port, max_frame,
backoff and connect_timeout (core/src/transport_vertex.cpp:parse_config documents the config shape;
kind is read at core/src/transport_vertex.cpp:cfg.name("kind") and the RFC-0014 §4 self-heal pair at core/src/transport_vertex.cpp:if (const auto v = cfg.u32("backoff")) s.backoff_ms = *v). There is no type pair
and no role key: the module segment in the path fixes both the transport and the role, and
kind — when spelled at all — only cross-checks the module’s own declaration. Extra transport kinds join the runtime catalog through
register_transport_type (core/src/transport_vertex.cpp:transport_vertex_t::register_transport_type(std::string_view kind, transport_factory_t factory,) — that is how the QUIC module
extends a node without this file ever learning about it.
The superseded global spelling write /net:children[] += SPEC{type, name, config} was retired
at #492 S7: the client and listener
child types are no longer registered, so that write answers SCHEMA_NOT_FOUND. :children[] as an
enumeration is untouched — /net:children[] still lists the modules and
/net/<module>:children[] still lists a module’s connections.
The write is ACL-gated. It is gated today on the creator endpoint’s ordinary vertex WRITE
right and denied with PERMISSION_DENIED otherwise. Under
RFC-0014 — creator endpoint, connection lifecycle and link liveness
that gate splits in two — SPEC (create) on CREATE, and its removal counterpart, a NAME
write, on WRITE — not DELETE — per
RFC-0009 — vertex removal and subscriber eviction §A.2.
Mount and routing are the same path. A created connection lives at /net/<module>/<name> and
routes by exactly that path: the routing key is the mount path, so the registry’s precomputed
NAME run is exactly the prefix a hop prepends to src and the forward path assembles nothing per
hop (core/src/transport_vertex.cpp:transport_vertex_t::make_connection_locked, core/src/transport_vertex.cpp:Compose the mount key;
ADR-0061 — per-transport mount routing, strip-K L5 demux).
The /net/<module> structural vertex is created lazily on first use, with graph_.find itself as the
dedupe rather than a second source of truth (core/src/transport_vertex.cpp:transport_vertex_t::make_connection_locked). Because a
connection is addressed under /net/<module>/, a first-level local vertex cannot shadow one.
Module naming is declared-only, by the application
(ADR-0073
§4). There is no derived default and no library-side auto-registration: linking a built-in
transport registers no module name, and an undeclared (kind, role) pair fails creation with
SCHEMA_NOT_FOUND (core/src/transport_vertex.cpp:transport_vertex_t::module_for). The application declares each module
under a name it chooses through register_module (core/src/transport_vertex.cpp:transport_vertex_t::register_module,
core/include/libtracer/transport_vertex.hpp:transport_vertex_t::register_module), a minting boundary gated by the shared
segment-validity predicate — a reserved-character name answers INVALID_PATH. The built-in
transports export suggested-name constants (kWsClientSuggestedModule, …) an application may
adopt; /net itself is likewise only the recommended root convention (a constructor default).
Creation is all-or-nothing. A connection is built in three steps — register the identity
vertex, insert the conns_ entry, wire the link into the router’s child_registry_t — and only
the last can be refused: add_child answers false when the registry cannot grow, and it is the
only place that can say so (core/include/libtracer/fwd_router.hpp:fwd_router_t::add_child,
core/include/libtracer/child_registry.hpp:child_registry_t::add). A refusal unwinds the first two in reverse —
retire the vertex, then erase the entry, which destroys the config-constructed socket — publishes
no liveness, and answers BACKPRESSURE (core/src/transport_vertex.cpp:transport_vertex_t::make_connection_locked). Discarding that
bool left a connection reporting UP that no dst resolved, no inbound frame reached, and
remove_child did not know about — a ghost a peer could mint by creating connections until the
registry slab exhausted. A provide_link staging is consumed only once the wiring has succeeded
(core/src/transport_vertex.cpp:(void)pending_links_.erase(staged_key)), so a retry after the pressure clears still finds its link.
Liveness is the connection vertex’s value. link_state_t is six states —
DORMANT, DIALING, RECONNECTING, UP, LISTENING, BIND_FAILED
(core/include/libtracer/transport_factory.hpp:link_state_t). DIAL links use the first four; LISTEN
links report listen-socket reachability with the last two, never a per-accepted-peer state. The
value is a 1-byte VALUE on the vertex, so it is await-able and subscribable: subscribe /net/<module>/<name> streams every transition. The liveness engine that would drive these
automatically is not implemented — the value is set by the caller, and a config-constructed socket
reports UP or LISTENING at creation (core/src/transport_vertex.cpp:transport_vertex_t::make_connection_locked).
The accepted direction, and what is not realised. RFC-0014 replaced the single global
/net:children[] catalog with the per-module creator endpoint at /net/<module>/conn, whose own
:schema is that module’s config catalog. The endpoint itself is implemented (S2b): declaring a
module mints it, a SPEC{name, config} written there creates /net/<module>/<name> and a
NAME{<name>} removes it, with transport and role positional. conn is hidden from
/net/<module>:children[] (S4) while staying addressable for the creatability probe, and the old
catalog is gone (S7 — see above), so there is one door rather than two. A module may
declare its conn:schema catalog at register_module (S3’s module-side half,
#1815): the endpoint serves it inside the
Amendment 3 SETTINGS and refuses a non-conforming SPEC TYPE_MISMATCH, while a module that
declares none answers the empty SETTINGS envelope. The CREATE/WRITE gating split (S2c) is
in; the link-liveness engine (S5, self_heal_link_t) runs for
kinds registered self_heal_dial, which since
#1548 includes the built-in
point-to-point DIAL kinds. One further
boundary is open by design: RFC-0014 delivers the link, while third-party multi-hop SUBSCRIBER
origination — making one node subscribe to another and then departing — is a separate unanswered
question, so an orchestrator can create the wires’ links in band without being able to
originate the wires (#491).
Connection settings are transport-private¶
conn_settings_t (core/include/libtracer/transport_factory.hpp:conn_settings_t) and conn_role_t (core/include/libtracer/link_kind.hpp:enum class conn_role_t) are
a device-private :settings facet of a connection vertex
(ADR-0021 — the colon-field plane is the vertex ioctl
draws the standard / device-private line). They live on the tr::net leaf record and are never
the L4 vertex :settings surface — conflating the two would put addr, port and kind into
the universal settings surface of every vertex in the graph. (That surface’s core namespace is
now empty anyway — RFC-0022 §3.B — which makes the separation structural rather than merely
disciplined.)
The record carries only the keys every transport kind shares. A kind’s private configuration —
a QUIC certificate and key path, for instance — never lands here; that kind’s own factory parses it
out of the raw config SETTINGS TLV handed to it alongside these settings.
Both families — the universal keys and every kind’s private ones — are tabulated key by key on connection config.
Inside the process a link is addressed by its connection vertex’s handle (the router’s connection-slot index), never by its name, and a bus peer by its peer_handle_t. There is no separate “link token”: the connection vertex’s handle is the link’s only identity.
Pitfalls¶
add_childreturningfalseis not advisory. A mount name of any width registers and resolves (#523) — the descent makes one registry pass and matches each slot against the prefix of that slot’s ownseg_count— so there is no width pitfall left. Whatadd_childdoes refuse, always and not only in debug, is a name no address could ever name: empty, containing an empty segment, or wider thangraph::kMaxSegments; it also refuses when the registry cannot grow.falsemeans nothing was registered, so a caller that ignores it wires a link that is audible on its transport and resolvable by nodst, whilesize()andlive_size()report the registry as healthy.remove_childis removal;link_downis departure. A link that merely dropped must takelink_down, which evicts subscriber edges and label state but keeps the registry entry. Callingremove_childon a reconnectingDIALlink permanently unroutes it, because under the RFC-0014 model a connection’s vertex outlives its socket. Conversely, callremove_childbefore destroying thetransport_t, or a forward can resolve a freed object.A NAME owns one receiver context, for the router’s life (#884).
remove_childtombstones the child’schild_rx_ctx_twhere it stands — it stays on the lock-free published chain, because a receive thread may be walking it, but it answers no lookup — andadd_childof that same NAME revives it rather than appending a second. Two consequences callers rely on: a re-added child resolves to its current tenancy on the bound path (connection_ref/hop_mintre-resolveconn_slotper registration, so a child that gained a connection vertex between registrations becomes bindable, and one re-added as a bus mount stops being), and create/remove churn on a stable name set neither leaks a context nor lengthens the chain the bound hop walks.receiver_ctx_count()is the assertable form of the second, the twin ofchild_registry_t::size()— which has had this rule since #494/#521.A sink’s
ctxmust outlive every possible delivery. Sinks fire on transport receive threads, possibly several concurrently, and clearing a sink is the only thing that stops future calls.materialize()returns an owner, not a span. Holdingreply.materialize().bytes()past the end of the full expression reads freed memory; bind theview_tto a named object first.Configure children and sinks before frames flow.
add_childand the sink setters are control-plane calls; the forward path deliberately takes no lock, so concurrent reconfiguration is not part of the contract.A bus link’s own connection NAME is not a routable next-hop. A
dstthat names a multi-peer link’s NAME with a residual below it — rather than naming one of its peers — is rejected withtr::path::invalid(0x0021), never forwarded (RFC-0020). Forwarding it would reach the bus endpoint’ssend(), which broadcasts, so one directed request would draw one reply per peer and scramble any client that correlates replies FIFO. Naming the mount exactly still terminates locally, and a peer-directed hop (/net/<module>/<name>/<peer>/…) still forwards — only the broadcast shape is refused.A stale
COMPACTlabel is dropped, never fatal. A label with no ingress binding on its link produces aHANDLE_NACKback to the producer, which re-advertises. An implementation that treats an unknown label as a protocol error breaks the self-heal.
The rest of the plane¶
The router is the orchestrator, but most of the plane’s surface is in the pieces it delegates to — and each of those was extracted precisely so its rules could be tested against hand-built frames with no live transport.
op_resolver_tis the terminus half. When adstnames a vertex on this node, it applies the operation (READ / WRITE / AWAIT, plus any:fieldselector) against the graph and builds the reply as a rope: one exactly-sized head segment prepended to refcount-clones of the vertex’s stored payload, never a serialize into a fresh buffer. It is local-only by construction — adstthat does not resolve locally is answeredNOT_FOUND, and hop-by-hop forwarding stays the router’s job.route_handle_tis the per-node label store behind delivery compaction. Read literally, “a delivery is a FWD WRITE” makes every streamed sample re-carry its full return route; a per-link label aliases that route instead, and each hop swaps the label the way a CAN ID is re-resolved against each bus. Binding is advertise-driven and re-advertise on reconnect is the self-heal. A flow that is not flagged for compaction allocates nothing here, which is what preserves the stateless-forwarder property for everything else.The frame view (
fwd_hdr_t,fwd_pre_t,dst_seg_walk_t,control_head_t,fwd_rebuild_t,stack_writer_t) is the offset-dispatch cluster the forward hop reads a frame by: one header read as absolute offsets, the forward-versus-terminus peeks, the control-frame head peek, a fixed-capacity stack byte writer, and the shrunk-dst/ grown-srchead rebuild. Everything is templated over a cursor concept and yields offsets, never spans, so the same logic serves a contiguous frame and a link-walking rope and the caller re-slices from its own cursor.sink_slot_t(tr::sink_slot_t— layer-neutral since #1049, when L4 took its three configuration seams into one; it has notr::netspelling) holds each of the router’s five observability/terminus sinks — reply, inbound-FWD, raw-frame, compact-delivery, stale-label. A sink is a{function pointer, context}pair set from a control thread and read on every transport receive thread, so the two halves have to be published as a unit: a torn read hands a newly installed function the previous sink’s context, and every sink casts that context. The slot is three words — a generation counter and the pair — published through the counter and read with plain atomic loads, so the frame path takes no lock and never spins: a reader that lands inside a publish reports no sink for that frame rather than waiting. An unset slot therefore costs one load, which is what the plain member it replaced cost, and the router serializes the five setters against each other with one mutex no reader ever takes. It is the observer-shaped sibling of the transport plane’sreceiver_slot_t, which owns the same discipline plus the delivery-tier select (transport) — and the size matters: a first cut that carried astd::mutexper slot put the two sinks the FWD path reads on separate cache lines andbench_forward_demuxcharged ~2% for it at 64 registered links.The grammar (
tr::wire::grammar) is the shared TLV header/trailer core underneath all of that; it is documented with the codec on frame-codec.
API reference¶
-
struct router_planes_t¶
The router’s allocation planes as ONE aggregate (RFC-0028 §8.2, slice 10) — what
fwd_router_t’s constructor takes in place of six positional, defaulted seams.Every member defaults to the process net sub-pool (
mem::net_source()/mem::net_backend(), #1777), sorouter_planes_t{}is the unbounded host router and a bounded node names only the planes it points at its own slab:tr::net::fwd_router_t router{graph, {.label_src = &labels, .rx = &rx_pool, .flat = &flat_pool, .egress = &flat_pool}};
Every plane must outlive the router, and every injected one must be thread-safe (several transport receive threads reach them concurrently). Since slice 10 a
mem_backend_tIS ablock_source_t, so one synchronized pool can serve a source plane and a backend plane.Public Members
-
mem::block_source_t *label_src = &mem::net_source()¶
The nothrow source the
route_handleLABEL TABLES draw from (#603 defect 1 / #873 family 3, ADR-0065 / ADR-0079 §Decision 4) — the library holds no buffer of its own. A bounded node injects a mem::pool_source_t over its static slab (one slab, whole stack — ADR-0039 §2); the default is the process-wide nothrow platform heap. Must outlive the router, and must be thread-safe: the label tables are written fromon_advertise, which runs on a transport RECEIVE thread and is driven entirely by a remote peer.This plane used to be a
std::pmr::memory_resource*. It could not stay one: a pmr resource cannot report exhaustion by value, so on the shipping-fno-exceptionsprofile a peer’s ADVERTISE storm against an abortingheap_resource_trebooted the node. A call site that passed its OWN resource — a node whose graph and router historically shared one pmr arena — migrates by pointing mem::pool_source_t’s SPAN CONSTRUCTOR at the same storage that resource was partitioning (#1493). It must NOT reach for the adapter that shape invites, ablock_source_twrapping the pmr resource: that wrapper’stry_alloccannot answernullptr, so it reinstates the very abort this parameter change removed. mem::block_source_t’s warning carries the full reasoning, including why a budget-tracking variant is declined too. Split fromrxdeliberately (ADR-0079’s per-plane default):rxis per-frame decode scratch and may legitimately be abump_source_t, while label state is LONG-LIVED and would monotonically fill one.
-
mem::block_source_t *rx = &mem::net_source()¶
The nothrow source the TERMINUS ARENA draws from (#588). Split from
label_srcbecause the arena is built from a peer’s frame, on the RX path, behind no ACL: astd::pmr::memory_resourcecannot report exhaustion by value, so on-fno-exceptionsits only failure mode isabort(). Drawing the arena from a mem::block_source_t instead makes an over-large frame aTLV_NESTING_TOO_DEEPrefusal, counted asarena_droppedand answered with an addressedSTATUS{BACKPRESSURE}that draws from no source at all (#1612). A bounded node points this at the same slab aslabel_src. Must outlive the router.
-
mem::mem_backend_t *flat = &mem::net_backend()¶
The byte backend EVERY rope flatten on the router’s forward AND terminus paths draws its owned
segmentfrom — the router’s own four (#730): the ingress control-frame sub-rope flattens (ADVERTISEroute,COMPACTpayload), the cold bus-name rejection flatten, and the per-deliveryCOMPACTegress flatten; PLUS the terminus resolver’s rope-tier flattens one call belowresolve_terminus_rope(#766) —view_node::ensure_cache(the per-node contiguous span everywire()/body()read of a multi-link TLV materializes) andview_node::own_wire(the ADR-0053 ⑤ ownership flatten) — which the router reaches by passing this pointer straight to its graph::op_resolver_t. Until #766 the terminus half drew from the global heap, so a fragmented request from a peer escaped the bound; the honest sentence now is that all rope flattens on the forward and terminus paths draw from the injected seam. Since #801 that sentence covers the SPAN-tier terminus too:arena_node::own_wire, the ADR-0041 §2 ownership copy a span-delivered request takes, is this backend’s as well — so the MCU terminus (a synchronous CAN/UART child delivers spans, not ropes) no longer escapes the bound on its ordinary WRITE. Still NOT every allocation the router path makes: the reply head segment and the arena are their own injections (egressandrx) — the head drew fromview::heap_alloc’s global heap until #795 gave itegressbelow, which is the reading #730 was filed about. Split fromrxbecause these are BYTE buffers with cache hooks and an owning refcount (a mem::mem_backend_t), not the arena’s raw blocks — the same splitgraph_tmakes between itsctland itsvalue_backend(ADR-0060). Until #730 all of them took mem::heap_backend by default, so a bounded node’s memory bound did NOT cover them; now a node that points this at its own slab bounds them all, and every flatten failure is answered by value — the forward-path frame is dropped (never stored empty), and a refused terminus flatten answers an addressedkind=ERROR STATUS{BACKPRESSURE}reply (or, when the refusal hit the reply’s own route bytes, a drop).An injected
flatMUST be thread-safe, with the same forcegraph_trequires of itsvalue_backend(ADR-0060 §2) — and for the same two reasons, both of which hold here. Three of the four sites run on a transport child’s RECEIVE thread and several children receive concurrently; the fourth runs on the WRITER thread inside the remote-delivery fan-out. And thesegmentthis backend hands out self-routes its reclaim on whichever thread drops the last reference, which is not in general the thread that allocated it. The defaultheap_backend()already is thread-safe. A bare mem::pool_t is NOT — its free list is a plainstd::size_thead and count with no lock and no atomic, so two receive threads can be handed the same slot — and must be composed with the target’s arch-selected synchronisation before injection:mem::synchronized_pool_t<>guards it with the build’s one reader guard — the hostmutex_guard_t, ortr::esp::critical_pool_t’s interrupt-maskedcritical_guard_ton an MCU. A bounded node points this at the same slab aslabel_src/rxonly through such a composition. Must outlive the router.
-
std::size_t max_label_bindings_per_link = 0¶
Ceiling on one link’s ingress table and, separately, its egress table (#603).
0⇒ unbounded, the default and the prior behavior. Without it the tables are peer-driven and grow to the whole 16-bit label space — megabytes per link on a 16 KB node. A full table refuses NEW flows, which then deliver over the full-routeFWD{WRITE}form; established flows are untouched. See route_handle_t::refused_bindings for the counter.
-
mem::mem_backend_t *egress = &mem::net_backend()¶
The byte backend the terminus REPLY’s egress-construction segments draw from (#795, ADR-0074): the reply head (peer-driven size — the swapped route bytes plus the inline tail) and, on a mint, the trailing 12-byte
PATH_REF. It is the last reply-egress byte source that escaped a bounded node’s slab (both folded READs’ POINT headers — the composed root’s and the":children"listing’s — are payload framing and draw from the graph’s ownvalue_backendseam instead: #831, closed) — the head was hard-wired toview::heap_alloc’s global heap, one allocation on every reply, peer-drivable and pre-auth reachable (the denied path builds a head too). A DEDICATED injection, deliberately NOT folded intoflat:flatis documented and sized against FLATTEN (payload) bytes, and a reply head is egress construction sized against ROUTE bytes, so wideningflat'scontract would silently re-scope a slab deployments already set for flattens (a node could begin refusing replies it used to send). Passed straight to the graph::op_resolver_t. The default is the net sub-pool; only a bounded node that points it at its slab gets the bound. A refusal degrades through the same empty-rope →or_backpressure→ addressedSTATUS{BACKPRESSURE}path OOM already takes — answered by value, never an abort. MUST be thread-safe on the same terms asflat. Must outlive the router.
-
mem::mem_backend_t *retained = nullptr¶
The backend for the two allocations a REMOTE SUBSCRIBE keeps for the LIFE OF THE SUBSCRIPTION — the source
SUBSCRIBERTLV and “the ONE route copy of the subscription’s
life” (ADR-0041 §2). A DEDICATED injection for the reason
egressis one:flatis documented and sized against per-operation FLATTEN bytes, and these are neither per- operation nor flattens. Their live set is the SUBSCRIPTION POPULATION, so a host that size- classesflatfinds the classes filled by a set that never returns and has nothing left for the churn they were cut for — the classes stop working exactly when a client is attached, which is the only time they are needed.DEFAULTS TO NULL, meaning “@p flat” — where both have always been taken. An un-injected router is byte-for-byte unchanged, so this costs nothing to ignore. A host that injects it can send these to a plain heap arm: one alloc and one free per subscription is not churn and wants no class.
Sized against the subscription population, NOT a concurrency factor. Exhaustion is answered by value — a
BACKPRESSUREon the subscribe, which simply does not bind. MUST be thread- safe on the same terms asflat. Must outlive the router.
-
mem::block_source_t *label_src = &mem::net_source()¶
-
class fwd_router_t¶
A stateless hop-by-hop FWD forwarder (RFC-0004 §A/§B, ADR-0035 slice 3).
Wires a local graph::graph_t (terminus op resolution, via an internal graph::op_resolver_t) to a set of named transport children. Configure the children (and optional reply sink) before frames flow; thereafter on_frame fires on the transports’ receive threads — possibly several concurrently — and holds no mutable routing state, so no per-request locking is required.
The counted cold-path drops behind @ref drop_stats (#1503 step 3)
std::atomic<std::size_t>and not plain counters becauseon_frameruns on SEVERAL transport receive threads at once and the router takes no lock on the frame path — a plain++here would be a data race by the memory model. Word-sized and RELAXED percore/STYLE.md§Introspection clause 5, so rv32 keeps a lock-freeamoadd.wand pulls no libatomic. Every bump sits on a COLD arm that has already decided to lose the frame; no success arm gains an instruction (ADR-0039’s steady-state hop is the referee).LAST in the class, and that is a measured placement, not tidiness. Declared beside
label_not_found_— their natural home — these seven words pushregistry_, therx_head_chain and the two sink slots the FWD span path reads on every frame apart by a cache line, andbench_forward_demuxcharged ~6% for it at 64 registered links. The sink slots’ own doc block records the same hazard from the other direction. Cold state goes behind the hot state it would otherwise displace.The armed-origin list (#1645)
Cold, so behind the drop counters for the reason they sit behind the hot state. The records are the CALLER’s storage; the router holds only this head, newest first.
origins_is written underorigin_m_and read lock-free only as the “anything
armed?” hint that keeps the reply terminus lock-free when nothing is.
The four injected seams, exposed
A host that injected a seam already holds it; a host that took a DEFAULT (the process-wide heap source / backend) could not reach it at all, so it could not read the
stats()#1503 step 2 put onmem::block_source_t. These mirrorgraph_t::control_source— a reference, never null, never owned, and observation only — nothing in the library reads its own seams back through them.Per-seam, never aggregated (ADR-0079): there is deliberately no combined census here.
-
inline mem::block_source_t &label_source() const noexcept¶
The source the
route_handlelabel tables draw from.
-
inline mem::block_source_t &rx_source() const noexcept¶
The DEFAULT terminus-arena / rx-scratch source. A child that carries its own (ADR-0067 §3) is not reported here — that one is the child’s.
-
inline mem::mem_backend_t &flatten_backend() const noexcept¶
The byte backend every rope flatten this router performs draws from (#730).
-
inline mem::mem_backend_t &egress_backend() const noexcept¶
The byte backend the terminus REPLY head and the mint egress bytes draw from (#795).
Public Types
-
using reply_fn_t = void (*)(void *ctx, const view::rope_t &reply)¶
Reply-terminus sink (on_reply):
ctx, then the FWD{REPLY} frame.
-
using inbound_fn_t = void (*)(void *ctx, std::string_view inbound, const wire::tlv_node_t &fwd)¶
Inbound-FWD observer (on_inbound):
ctx, inbound child, the FWD read in place (a validated, non-owning wire::tlv_node_t).
-
using raw_fn_t = void (*)(void *ctx, std::string_view inbound, std::span<const std::byte> frame)¶
Raw-frame observer (on_raw):
ctx, inbound child, the whole frame.
-
using compact_delivery_fn_t = void (*)(void *ctx, std::span<const std::byte> route, std::span<const std::byte> payload)¶
Local COMPACT delivery sink (on_compact_delivery):
ctx, the bound local route PATH bytes, the delivered payload TLV bytes.
-
using stale_label_fn_t = void (*)(void *ctx, std::string_view inbound, std::uint16_t label)¶
Stale-label observer (on_stale_label):
ctx, inbound child, label.
Public Functions
-
inline explicit fwd_router_t(graph::graph_t &graph, const router_planes_t &planes = {})¶
Bind to the local
graph; terminus ops resolve against it.Also installs the graph’s remote-delivery sink (#136): a write to a vertex with a remote subscriber fans out a
FWD{WRITE}(or auto-promotedCOMPACT) back over the subscriber’s link. The sink capturesthis, so the router must outlivegraph'suse — the same lifetime the heldgraph_reference already requires.- Parameters:
graph – The node’s local graph.
planes – The router’s allocation planes (router_planes_t); every member defaults to the process net sub-pool. Each must outlive the router.
-
~fwd_router_t()¶
Answer nothing more: cancel every deferred AWAIT (ADR-0084), wait for any reply already being sent. Then disarm every origin_t still armed on this router (#1645).
-
std::size_t pending_awaits() const¶
How many remote AWAITs this router holds right now (ADR-0084): armed waiters plus any whose reply is being sent.
Each one is charged to the rx source of the link it arrived on, so this is also the count of waiter blocks those sources have lent out.
-
inline void configure_path_labels(path_label_table_t *labels) noexcept¶
Inject the RFC-0027 PATH-LABEL mint table, turning label switching ON for this node (§8.3) — or pass
nullptr(the default) to leave it off.Off is the conformant default, not a degraded one (§6.3). A router with no table never mints, so every hop’s local part travels as the string it travels as today and nothing on the route notices; a peer that presents a label to such a node gets §7.2’s
NOT_FOUNDand falls back to the full-string path it still holds. That is why this is aconfigure_verb and an injected pointer rather than a constructor parameter with a capacity: the bound comes from the embedder’s own store (ADR-0079’s per-plane axis) and the library chooses neither the capacity nor the ceiling (CONTEXT.md§Resource bound).It is also what keeps the judging lens honest. The plain-string forwarding path — the shape every deployment already ships — tests this one pointer against null on a member already in the router’s first cache line, and takes the not-taken branch. Nothing else about a string-only hop changes: no field is added to
fwd_rebuild_t(whose 256-byte ratchet is MEASURED, #1235), no byte is read that was not read before, and the mint work itself is out of line.Call during setup, before frames flow, on the same terms as the constructor’s seams.
labelsmust outlive the router.
-
inline path_label_table_t *path_labels() const noexcept¶
The injected mint table, or null when this node does not mint (§6.3).
-
inline std::size_t label_not_found() const noexcept¶
Labelled
dstelements this node refused and answeredNOT_FOUNDfor (§7.2).The counter
dispatch_edge_target’starget_canonical_resolves()is one seam out (core/src/graph.cpp): a refused deref is *”this answer is no longer trustworthy”*, and the discipline that precedent fixes is that the event is COUNTED rather than merely handled. Non-zero means peers are presenting labels this node cannot validate — stale ones after a teardown (the designed, self-correcting case) or labels it never minted. It is never an error: the sender’s recovery is the canonical path it still holds.
-
inline std::size_t label_resolves() const noexcept¶
Labelled
dstelements this node dereferenced and forwarded on (§7.2’s other side) — the numerator to label_not_found’s denominator.
-
inline std::size_t reply_name_lookups() const noexcept¶
Terminus replies whose egress link was found by NAME rather than taken from the receive context the request arrived on (#1709).
A request that arrives through a point-to-point child’s receiver is answered through that child’s link directly, and never moves this counter. It moves only where a name is all the router has: a BUS frame, whose inbound name is the sending peer’s and resolves through ADR-0044’s peer fallback, and a frame pushed through the public by-name on_frame door, which carries no receive context. A count that grows on a deployment with no bus children and no by-name ingress is a defect, never a correctness one. Monotonic and relaxed (
core/STYLE.md§Introspection).
-
inline router_stats_t drop_stats() const noexcept¶
One snapshot of this router’s counted cold-path drops (#1503 step 3).
See router_stats_t for what each field means and why the drops are counted HERE rather than through
graph_t::count_external_drop. Snapshot coherence is thecore/STYLE.md§Introspection clause: six relaxed loads, so use the difference between two snapshots, never the instant.
-
bool add_child(std::string_view name, transport_t &link, mem::block_source_t *rx = nullptr, link_kind_t kind = {})¶
Register a named transport-child vertex (ADR-0027).
nameis the mount RUN by which THIS node addresseslink— both the leadingdstsegments that route onward throughlink, and the run prepended tosrcwhen a frame arrives onlink(the way back). Installs a receiver onlinkthat funnels each inbound frame toon_frame(name, ...). Call once per link, during setup (before frames flow).Installs the receiver matching the link’s capability (ADR-0042 §1, ropes per ADR-0053): an owning link (
link.delivers_ropes()) gets a rope receiver whose frame funnels through the SAME routing (the forward hop stays span-based zero-heap; the refcounts ride only to the terminus); every other link keeps the borrowed-span receiver unchanged.may have ANY number of segments —
"up","ws-server/up", the RFC-0014 mount"net/<module>/<name>", or something far deeper. The 1..3 bound is GONE (#523): the mount descent no longer tries key widths from a compile-time constant downward, it makes ONE pass over the registry and matches each slot against the prefix of that slot’s own width. Mount width is therefore bounded by the path-depth budget every address spends from, and by nothing else — there is no constant to raise.The one thing still refused, and now refused ALWAYS rather than in debug builds only, is a name no address could ever name: empty, containing an empty segment, or wider than
graph::kMaxSegments(adstcannot carry that many segments, so nothing could ever prefix-match it). Such a name used to append a slot thatsize()andlive_size()reported as a healthy child while every forward to it missed and fell through to the terminus with no error anywhere — the #516 failure shape, one layer up.A NAME owns exactly one receiver ctx, for the router’s life (#884). Registering a name that already has one — a re-add after remove_child, or a duplicate add of a live name — REBINDS that ctx rather than appending a second, the same one-slot-per-name rule child_registry_t::add follows and for the same two reasons. A second ctx would shadow the first on every name-keyed lookup (which returns the FIRST match, so the DEAD one), and churn on a stable name set would grow the published chain without bound.
[[nodiscard]]since #892, and the reason it was NOT is worth recording because it was wrong in a way that shipped a bug. The attribute was declined on the ground that “every
existing call site registers a name it composed itself, so it stays correct ignoring
it” — true of the
unaddressable-name half, and false of the other half the return value also carries.child_registry_t::addreturns false when the table could not grow, which no composed name can rule out;make_connectiondiscarded exactly that and minted a connection published UP but resolvable by nodst(#930). With the attribute, that bug is a compile error rather than a silent ghost.A caller that genuinely cannot act on the failure writes
(void)and says so. That is the point of the attribute: it does not forbid ignoring the result, it makes ignoring it a deliberate, greppable act instead of the default.The router’s own registration state — the receiver ctx, a bus mount’s token cache and an interned link kind — is drawn from the LABEL plane’s source (
router_planes_t:: label_src, the net sub-pool by default; #1779). A refusal there is the samefalse, with nothing registered.- Parameters:
name – This node’s local mount name for the link (e.g. “up”, “ws-server/up”).
link – The transport carrying the next/previous hop.
rx – Optional per-child failable-block source; null uses the router’s. Give each child its OWN when injecting a bounded one — see ADR-0067 3 for why sharing one across receive threads is the wrong shape.
kind – The link’s transport-catalog
(kind, role)(#1650), which every write this child carries presents to the target’s admission filter and handler asgraph::write_ctx_t::link. Copied and interned here, ONCE per registration, so a frame carries a pointer and pays no lookup; an emptykind.kind(the default) registers a link with no catalog identity, whose writes present a nulllink.transport_vertex_tpasses its connection’s declared pair.
- Returns:
false ⇔
nameis unaddressable, or the registry could not grow — either way NOTHING was registered.
-
bool remove_child(std::string_view name)¶
Un-register child
name— it stops resolving, and its routing state goes.The teardown counterpart of add_child (#494). Tombstones the registry entry (child_registry_t::erase) and then runs the full link_down eviction, so the subscriber edges and route-handle labels bound to the name leave with it.
This is removal, not departure. link_down alone is the right hook for a link that merely dropped: under RFC-0014 a DIAL connection’s vertex outlives its socket and self-heals, so evicting its registry entry on a down notification would permanently unroute a link that is only reconnecting. Call this one when the connection itself is gone — and call it BEFORE destroying the
transport_t, so no forward can resolve a freed object.The child’s receiver ctx is TOMBSTONED, not unlinked (#884): it stays on the published chain — a lock-free reader may be walking it right now — but stops answering every name-keyed and slot-keyed lookup, and a later add_child of the same name revives it in place. That is what keeps
connection_ref/hop_mintoff the dead context after a re-add, and what keeps create/remove churn on a stable name set from growing the chain.- Parameters:
name – The child’s registered NAME.
- Returns:
true if
namenamed a live child, false if it named none.
-
graph::result_t<void> subscribe_toward(const graph::path_t &producer, const graph::path_t &target)¶
Bind a local producer’s subscription toward a MOUNT-PATH target — the locally-initiated dual of the wire
:subscribers[]append (#739).targetis one ordinary path that routes through a transport mount (e.g./net/ws-client/b/display/val, arbitrarily nested/net/A/net/B/x). It is resolved through the SAME strip-K cached descent the forward path uses (ADR-0061), so the caller never hand-splits(link, return route)— the split that bakes in a single-hop assumption and thenet/<module>/<name>string shape. The residual below the matched mount becomes the delivery route the first hop forwards, exactly as an inboundFWDwould carry it.Bind-time resolution, link-lifetime durability. The
(link, route)split is computed ONCE, here. If the link later tears down, the binding is evicted with it (link_down) and re-binding is the application’s job — this helper does not track topology churn (re-establishment is #716’s question, not this API’s).A target whose first hop lands on a bus PEER (
/net/ws-server/mesh/p0/...) is rejected withINVALID_PATH: the per-delivery sink resolves the stored link by its registry NAME, and a peer has no directed registry entry to store — binding it would produce a subscription that silently broadcasts. Directed bus-peer delivery arrives with the #741 work.- Parameters:
producer – The local producer vertex’s path.
target – The mount-path target, spelled from THIS node’s root.
- Returns:
NOT_FOUNDifproducernames no vertex;INVALID_PATHiftargetdoes not route through a mount (or lands on a bus peer);BACKPRESSUREon allocation failure; otherwise the graph::graph_t::subscribe_wire admission result.
-
graph::wire_target_split_t split_subscriber_target(std::span<const std::byte> key) const¶
Split a subscriber target key at the mount it routes through (RFC-0021 §4.B) — the descent subscribe_toward and the wire
:subscribers[]door share.The one place “does this target leave this node, and over which link” is decided, so the host-local dual and its wire twin (#491) cannot drift. Runs the ADR-0061 strip-K descent over the child registry, exactly as the forward path resolves a frame’s
dst.- Parameters:
key – The target’s canonical key — concatenated
NAMErecords. Its bytes must outlive the returned split, whoseresidualborrows from them.- Returns:
{}(empty link, not unroutable) ⇔ NO mount matched — the target names something this node terminates;unroutable⇔ a mount was named but no directed delivery can be bound through it; otherwise the mount’s registry NAME and the residual below it.
-
std::optional<wire::path_ref_element_t> connection_ref(std::string_view link_name) const¶
This node’s own vertex ref for the connection vertex of child
link_name— element 0 of a route that leaves through it (RFC-0024 §4.1).The origin mints its OWN first element, because no one else can: an element is node-scoped, and the hop out of this node is the one hop no peer ever sees. A mint reply therefore comes back one element short of the route, and this is the element that completes it (which adopt_binding does for the caller).
- Return values:
std::nullopt –
link_namenames no registered child, the child has no connection vertex in this node’s graph, or that vertex’s generation has SATURATED (permanently unbindable, §4.4 rule 3). Every one of those means the route has no bound spelling from here, and the canonical path is the answer.
-
transport_t *bound_egress(wire::path_ref_element_t e, std::string_view caller, graph::acl_right_t right) const¶
The egress link a bound element names, after the full §5.1 check.
The forwarder’s hop and the origin’s first hop are the same act — consume element 0, dereference it, egress — so they are the same function. It bounds-checks the index, compares the generation, refuses a saturated one, and evaluates the ACL at the dereferenced vertex for
right(§6.2: a generation match authorizes nothing).- Parameters:
e – The element to consume.
caller – The subject context — the inbound link’s name at a forwarder, empty for this node’s own local caller at an origin.
right – The right the operation carries.
- Return values:
nullptr – Any part of the check failed, or the vertex is not a connection vertex of a live point-to-point child of this node. The caller MUST then drop — never repair, never fall through to a different route (§5.3). A bus PEER is among the refusals and not by omission: a peer has no vertex, so no element can name one, and egressing over the bus link itself would BROADCAST a directed operation (ADR-0073 §3 / RFC-0020).
-
bool adopt_binding(graph::path_t &path, std::string_view link_name, const wire::tlv_node_t &reply)¶
Install the bound form on
pathfrom the mint answer on a reply (RFC-0024 §7.4).The origin’s side of the exchange, and the reason
path_t::bindhad no production caller until now.replyis a validatedFWD{REPLY}node (read in place, #1829); its LAST child is the accumulatedPATH_REF— the terminus’s element, with one prepended by every hop that forwarded the reply. This prepends THIS node’s own element forlink_name(§4.1: element 0 is the origin’s reference to its first-hop connection vertex) and records the whole stack.Nothing is installed unless the complete route is spellable: a reply with no mint, a link with no bindable connection vertex, or a stack past the normative element cap all leave
pathexactly as it was — bound to nothing, and therefore canonical. A binding is an optimisation with a fallback that always works, so a partial one is never worth having.- Returns:
true iff
pathcame out bound.
-
std::optional<bound_dispatch_t> bound_dispatch(const graph::path_t &path, graph::acl_right_t right) const¶
Spell the next operation over
path'sbinding — the origin’s own hop (§4.1).The origin consumes element 0 exactly as every forwarder consumes its own: it is this node’s reference to its first-hop connection vertex, so it selects the link and does NOT go on the wire. What goes out is the residual,
4 + 8×(H−1)bytes, and the host that receives it consumes ITS element in turn — the monotone shrink that makes a bound path loop-free for the same reason a canonicaldstis.- Parameters:
right – The right the operation carries, evaluated at the dereferenced connection vertex like any other hop’s (§6.2).
- Return values:
std::nullopt –
pathis unbound, or element 0 no longer validates — a link removed, a connection vertex retired, an ACL revoked — or the label plane’s source refused thedstbytes. The caller’s recovery is the one that always works:clear_binding()and send the canonical path it still holds, which may then re-mint (§5.3).
-
bool adopt_path_label(graph::path_t &path, std::string_view link_name, const wire::tlv_node_t &reply)¶
Cache the minted
srcofreplyaspath'slabelled spelling (§6.1 point 4).The analogue of adopt_binding, and the seam erratum 2 named and left unruled.
replyis a validatedFWD{REPLY}node; itssrcis the address as the route spelled it on the way back — a label element for every hop that minted, the literal mount run for every hop that did not (§5.2, §6.3). Those bytes are cached verbatim, with the origin’s own first-hop local part (the mount run oflink_name, spelled as literal segments) prepended, so the cached body is this node’s spelling of the same address the canonical bytes spell.There is no normalization and no completion pass: a mixed spelling is the expected steady state, not a degraded one, and improving it is not this seam’s job — carrying it is.
The validation is
graph::path_t::cache_path_label’s and nothing is added to it: a body with no label element, one that does not walk cleanly, one pastkMaxPathBytes, and a path alreadyPATH_REF-bound (§11.2) are its refusals, and each leavespathexactly as it was — canonical, which always works.- Parameters:
path – The path the operation addressed, still holding its canonical bytes.
link_name – This node’s registry NAME for the child the request left over.
reply – The validated
FWD{REPLY}node that came back.
- Returns:
true iff
pathcame out carrying a labelled spelling.
-
std::optional<label_dispatch_t> label_dispatch(const graph::path_t &path) const¶
Spell the next operation over
path'scached label spelling — the origin’s own hop.The origin consumes its own first-hop local part exactly as a forwarder consumes its own, and for the same reason: it selects the link and does NOT go on the wire. Here that part is a run of LITERAL segments (the origin mints nothing), so the run is resolved by the ordinary strip-K mount descent — the same one a canonical
dsttakes (ADR-0061) — and what goes out is the residual, which is where the labels are. That residual is what makes §7.2’s deref reachable in a deployment at all; until this existed, every minted label was cached and then thrown away.No ACL right is taken, and the asymmetry with bound_dispatch is deliberate: element 0 of a binding is a peer-visible reference that must be dereferenced and re-checked (§6.2), whereas the head consumed here is a NAME this node resolves locally, exactly as it resolves the same name in the canonical spelling. Every hop that reads a label re-checks the ACL at the vertex it dereferences (§8.2), which is where a labelled operation’s authorization is decided.
- Return values:
std::nullopt –
pathcarries no cached spelling, its head names no mount of this node (or a bus mount, which is never labelled), or the residual has no spelling. The caller’s recovery is the one that always works: send the canonical pathpathstill holds.
-
void on_reply(reply_fn_t fn, void *ctx = nullptr) noexcept¶
Set the sink for a REPLY that terminates at this node’s reply endpoint.
Invoked (with the
FWD{REPLY}frame as a view::rope_t) when a REPLY’s firstdstsegment does not name a transport child — i.e. the accumulated return route has been fully consumed and this node is the originator. Optional; absent ⇒ such a reply is dropped.The frame is handed over rope-native (ADR-0055): the router performs NO decode and NO flatten — a rope-delivered reply reaches the sink zero-copy. A sink that wants contiguous bytes holds
const view_t m = reply.materialize()and readsm.bytes()— a single-link reply (the common case) is returned zero-copy, no alloc, no copy; only a multi-link reply pays one flatten, on demand. A sink that reads the frame walks those bytes in place (wire::tlv_node_t::over(m.bytes())). The materialize escape hatch (ADR-0052) now lives at the consumer, not the router; keepmalive while reading its span.- Parameters:
fn – Callback invoked on a transport receive thread; keep it cheap.
ctx – Opaque pointer handed back as
fn'sfirst argument.
-
void on_inbound(inbound_fn_t fn, void *ctx = nullptr) noexcept¶
Set a read-only observer of every inbound FWD (observability/tests).
Invoked with the inbound child name and the FWD, before routing. Carries no routing semantics; used to assert the per-hop
dst-shrink /src-grow invariant and as the seam where a per-hop:aclforward-right check (RFC-0004 §F) will later hang.The FWD is handed over as a wire::tlv_node_t (#1648): the whole frame validated once, CRC trailers included, then read in place — observing allocates nothing for a frame nested no deeper than the walk’s inline slots. The node borrows the inbound bytes, so it is valid only for the duration of the call; an observer that keeps anything copies it (
fwd.bytes()).- Parameters:
fn – Callback invoked on a transport receive thread.
ctx – Opaque pointer handed back as
fn'sfirst argument.
-
void on_raw(raw_fn_t fn, void *ctx = nullptr) noexcept¶
Set a read-only observer of every inbound frame’s RAW bytes (any type).
Fires before dispatch with the inbound link name and the complete frame span — used by tests to measure the on-wire byte-delta between a lean COMPACT delivery and the equivalent full-route FWD{WRITE} (the point of the route-handle).
- Parameters:
fn – Callback invoked on a transport receive thread.
ctx – Opaque pointer handed back as
fn'sfirst argument.
-
void on_compact_delivery(compact_delivery_fn_t fn, void *ctx = nullptr) noexcept¶
Set the sink for a label-compacted delivery that terminates at this node.
Invoked when a COMPACT’s label resolves to a LOCAL terminus binding (the established route names a vertex here): the payload has already been written to that vertex (delivery-is-a-write, RFC-0004 §D). Carries the bound local route PATH bytes and the delivered payload TLV bytes (both borrowed for the call).
- Parameters:
fn – Callback invoked on a transport receive thread; keep it cheap.
ctx – Opaque pointer handed back as
fn'sfirst argument.
-
void on_stale_label(stale_label_fn_t fn, void *ctx = nullptr) noexcept¶
Set the observer for a dropped stale/unknown-label COMPACT (RFC-0004 §E.1).
Invoked when a COMPACT bears a label with no ingress binding on its link — the frame is dropped and a HANDLE_NACK is sent back to prompt a re-advertise (never a crash). Carries the inbound link name and the stale label.
- Parameters:
fn – Callback invoked on a transport receive thread.
ctx – Opaque pointer handed back as
fn'sfirst argument.
-
std::uint16_t advertise(std::string_view link_name, std::span<const std::byte> route_path)¶
Advertise a
label ↔ routebinding over linklink_name(producer side).Sends an ADVERTISE carrying
routeand records the egress binding (so a NACK can re-advertise). Call when a compact-flagged flow starts or on (re)connect — re-advertising IS the self-heal, and the label is minted once per(link, route)then REUSED (#913): the frame goes out on every call, but a re-advertise loop grows no label or table state.- Parameters:
link_name – This node’s NAME for the downstream link to advertise over.
route_path – A complete PATH TLV’s bytes — the delivery route to alias.
- Returns:
The label to stamp on subsequent send_compact, or 0 if
link_namenames no child, or that link’s label space is exhausted / its egress table full (#603 — seeroute_handle_t::ensure_egress). No ADVERTISE is sent in either case.
-
void send_compact(std::string_view link_name, std::uint16_t label, std::span<const std::byte> payload)¶
Send a label-compacted delivery over link
link_name(producer side).Emits
COMPACT{ label, payload }— the route does NOT ride, only the label bound by a prior advertise. No-op iflink_namenames no child.- Parameters:
link_name – This node’s NAME for the downstream link.
label – A label returned by advertise for that link.
payload – A complete payload TLV’s bytes (the delivered VALUE).
-
void clear_link(std::string_view link_name)¶
Forget all route-handle label state for link
link_name(self-heal hook).A transport calls this on (re)connect/disconnect; a subsequent re-advertise rebinds cleanly and a delivery on a now-cleared label is NACK’d, not misrouted.
On a MID-CHAIN node this also drops every ingress binding held on ANY link whose downstream half crossed
link_name(#716) — seetr::net::route_handle_t::clear_link. Without it the upstream, which never saw the reconnect, keeps streaming COMPACTs onto a dead out-label and the flow drops silently forever; with it the upstream’s next COMPACT draws the ordinary stale-labelHANDLE_NACKand the flow re-advertises itself back up.- Parameters:
link_name – This node’s NAME for the link whose label state to drop.
-
void link_down(std::string_view link_name)¶
The link-departure hook (RFC-0009 §D extended to peer departure): evict everything the routing plane holds against a link that just died.
Two halves, in order:
graph_t::evict_link_edges(link_name)deactivates and reclaims every subscriber edge whose stored link islink_name(write fan-outs stop addressing the dead session, and its ~90 B/edge of route/link/caller state is released — the C6’s measured ~27 KB/browser-session leak), then clear_link drops the link’s route-handle label state (unchanged self-heal semantics).add_childinstalls this automatically behind every child’s departure notifier (transport_t::set_down_notifierwith the child’s registered NAME; the bus facet’sbus_link_t::set_peer_down_notifierwith the departed PEER’s name — the same name inbound frames were tagged with, hence the name subscriber edges stored). A host that learns of a departure out-of-band (its own session manager) may call it directly; calling it for a live or unknown link is safe (the next delivery-compact flow re-advertises; eviction of nothing is a no-op).Runs on the calling thread (typically a transport receive/close thread) and takes graph locks — callers must hold no transport-internal locks (the
set_down_notifiercalling discipline).- Parameters:
link_name – The routing plane’s inbound NAME for the departed link/peer.
-
inline const route_handle_t &handles() const noexcept¶
The route-handle label store (test introspection — assert statelessness).
-
bool sample_stats(std::string_view seam_class, std::string_view seam_name, graph::stats_block_t *out) const noexcept¶
Sample one NET-PLANE
:statsseam (RFC-0010 Amendment 2, #1503 residual).The body behind the sampler this router installs on the graph in its constructor; public so an in-process supervisor can ask for the same block the wire door serves, without re-deriving the seam vocabulary from the raw accessors.
The classes, and the nouns each publishes:
router.drops— drop_stats, all seven per-cause counters.labels.table— the RFC-0027 label plane:labels_exhausted,refused_bindings,label_not_found,label_resolves.link.<child>— one registered child, by itschild_registry_tNAME (the same key as its/net/<module>/<name>connection vertex):dropped_rx,malformed_rx,dropped_txfrom transport_t::drop_stats, pluslabels_usedwhen label switching is on for this node. A name that is not registered, or whose slot is a tombstone (a removed link), is NOT a seam — the caller getsfalse.
- Parameters:
seam_class – The seam CLASS.
seam_name – The seam NAME within it.
out – Where to write the block, or
nullptrto PROBE recognition only — which is how the graph settles NAME validity above its READ gate without sampling anything for a caller it will refuse.
- Returns:
trueiff the spelling names a seam this router serves.
-
void on_frame(std::string_view inbound_name, std::span<const std::byte> frame)¶
Route one inbound FWD frame that arrived on child
inbound_name.Forwards (dst-shrink + src-grow) toward a transport child, resolves+replies for a local terminus, or delivers a terminal REPLY to the reply sink. A malformed or non-FWD frame is dropped. Never blocks on a downstream peer beyond the transport’s own bounded send.
Runs on the calling (transport RX) thread and takes graph and router locks — the caller must hold no transport-internal locks across this call.
- Parameters:
inbound_name – This node’s NAME for the link the frame arrived on.
frame – The complete inbound FWD frame bytes (borrowed; consumed before this call returns — sends happen inline).
-
graph::result_t<void> originate(origin_t &slot, graph::fwd_op_t op, const graph::path_t &dst, std::span<const std::byte> payload = {}, const graph::path_t *reply_to = nullptr)¶
Originate a
READorWRITEfrom inside this node, through the SAME ingress a frame arriving on a link takes — without building wire bytes and without feeding on_frame under a made-up child name.Builds
FWD{ op, dst, FIELD?, src, payload? }(RFC-0004 §B) into ONE exactly-sized segment from the injected egress backend (router_planes_t::egress), armsslot, and routes the frame with no inbound link: a forward hop strips the mount fromdstand growssrcby nothing (this node is the origin), and adstthat names no mount is resolved here, its reply handed straight toslot. The segment is released before this returns; the router keeps no buffer and no per-request state.An append is a
WRITEwhosedstcarries an append selector —/…/vertex:subscribers[]— and so needs no opcode of its own:dst's:fieldtail rides as theFIELDselector.AWAITis not originated here.The reply is paired by its return route. Without
reply_tothe router spells a fresh one-segment route per request (a reserved~o<hex>NAME, valid as a segment and never a mount), so pairing is exact. Withreply_tothe request’ssrcis that local path — which a subscribe needs, because the producer delivers to itssrc— and requests sharing one route pair oldest-first, the arrival-order fallback the reference states for replies their route cannot tell apart. A reply no armed record matches goes to the on_reply sink, as before.- Parameters:
slot – The caller’s record; must not already be armed.
op –
READorWRITE.dst – The target, spelled from THIS node’s root (
/net/<module>/<name>/…), optionally with a:fieldtail.payload – A
WRITE’s value — ONE complete TLV, copied into the frame. Empty for aREAD.reply_to – Optional local return route; must not route through a mount. When given, its bytes are borrowed and it must outlive the armed record.
- Returns:
Success once the frame has been routed (a reply may already have been delivered);
TYPE_MISMATCHfor any otherop, or aWRITEwithout a payload;INVALID_PATHfor an emptydstor an unusablereply_to;BACKPRESSUREwhenslotis already armed or the egress backend refuses the frame.
-
bool cancel(origin_t &slot) noexcept¶
Disarm
slot— the caller’s deadline expired, or it no longer wants the reply.- Returns:
trueiffslotwas armed: its sink will not run for this request.falseif it was not armed — its reply was, or is being, delivered.
-
inline const child_registry_t ®istry() const noexcept¶
The connection registry (test introspection — the shared demux table).
-
inline std::size_t receiver_ctx_count() const¶
How many receiver contexts this router holds (test introspection — the churn bound of #884): one per NAME ever registered, live or tombstoned.
The twin of child_registry_t::size
, and asserted against for the same reason: it is the length of the chain every name-keyed and slot-keyed lookup walks, so “create/remove
churn does not grow it” is a latency property as well as a memory one. Takes the control lock, so it is a control-plane call — never a per-frame one.
Public Static Functions
-
static bool fall_back_on_label_refusal(graph::path_t &path, const wire::tlv_node_t &reply)¶
§7.2’s fallback: on a
NOT_FOUND-class refusal, droppath'slabelled spelling.A label a hop cannot validate answers
tr::path::not_found, and the sender’s whole recovery is to fall back to the full-string path it still holds, forget the bytes, and re-mint from the next reply. One failed operation is the entire cost.There is no withdraw frame, no unbind, no lease, no TTL, no repair and no retry against a different slot (§7.3, §7.2) — this clears a cache and nothing else. Static because it is: the recovery reads
replyand touches no router state, because no router holds any for a label it was handed.- Returns:
true iff
replywas atr::path::not_foundrefusal and a cached spelling was dropped — i.e. the caller should re-send canonically.
-
static session_anchor_id_t session_anchor_id(std::string_view mount, std::string_view peer) noexcept¶
The node-scoped identity string of the session
peerholds on bus mountmount— the keygraph_t::register_session_anchorallocates against (#1223).Deliberately NOT a spellable address. It is
:<mount>/<peer>, and both:and/are among the seven characterspath::valid_segmentrejects, so the anchor’s rendered key is bytes no registered path in this graph can produce — the property that keeps an anchor out of every listing, every descent and every sweep-set collision. Qualified by the mount because two listeners each name their first sessionp0, and those are two sessions.Exposed because it is what a test (and a later step’s mint) needs in order to name the anchor the router created; it is a pure function of its arguments and holds no state.
Returned BY VALUE in a fixed buffer, with no allocation (#1779): the anchor key is ONE packed segment record, so an id longer than
wire::kPackedSegMaxBytescould never be stored anyway. Such an id comes back EMPTY, and the router anchors nothing for it.
-
struct bound_dispatch_t¶
What the origin sends a bound operation as: the link, and the
dston the wire.Public Members
-
transport_t *link = nullptr¶
The egress link element 0 named.
-
mem::block_array_t<std::byte> dst¶
The
PATH_REFTLV carrying the RESIDUAL, drawn from the label plane’s source (#1779).
-
transport_t *link = nullptr¶
-
struct label_dispatch_t¶
What the origin sends a labelled operation as: the link, and the
dston the wire.Public Members
-
transport_t *link = nullptr¶
The egress link the literal head resolved to.
-
mem::block_array_t<std::byte> dst¶
The
PATHTLV carrying the labelled RESIDUAL, drawn from the label plane’s source (#1779).
-
transport_t *link = nullptr¶
-
class origin_t¶
The caller-owned record of ONE request this node originated with originate.
The requester owns correlation and deadline (RFC-0004 §B: forwarders are stateless), so the state an outstanding request needs lives HERE, in storage the caller provides, and not in the router. The router threads outstanding records through an intrusive list it only borrows; it allocates nothing per request and keeps nothing once a record leaves.
A record is armed by originate and disarmed exactly once, by whichever comes first: the matching
FWD{REPLY}(the sink runs, then the record is free again), or cancel — which is how the caller’s own deadline ends a request. There is no timer and no clock anywhere in the router; “how long to wait” is the caller’s decision. Destroying an armed record cancels it.Not copyable and not movable: the router holds its address while it is armed.
Public Functions
-
inline origin_t(reply_fn_t fn, void *ctx) noexcept¶
A record whose reply goes to
fnwithctx.- Parameters:
fn – Called once with
ctxand the matchingFWD{REPLY}frame, rope-native and with the same contract as the on_reply sink. Runs on the thread that delivered the reply — a transport receive thread, or the caller’s own thread inside originate when the reply is produced synchronously (a local terminus, a synchronous link). The record is already disarmed when it runs, so it may re-originate on the same record.ctx – Opaque pointer handed back as
fn'sfirst argument. It must outlive every dispatch: when cancel returnsfalsea reply was, or is being, delivered, and the caller synchronizes with its ownfn.
-
~origin_t()¶
Cancels the record if it is still armed.
-
inline origin_t(reply_fn_t fn, void *ctx) noexcept¶
-
inline mem::block_source_t &label_source() const noexcept¶
-
struct router_stats_t¶
The router’s counted cold-path drops — one snapshot of every frame this router lost rather than forwarded (#1503 step 3).
Vocabulary and polarity per
core/STYLE.md§Introspection; the snapshot-coherence clause stated there applies verbatim (each field is one relaxed load, so a multi-field snapshot may tear — the intended use is the difference between two snapshots).Every field counts a **
dropped**, not arefused: these sites lose a frame and tell nobody, which is exactly why the doctrine requires them counted. They are ROUTER-LOCAL and deliberately NOT folded throughgraph_t::count_external_drop— these frames die before the graph is involved, and that door’s exclusion rules exist to stop one refusal being tallied on both sides of the net/graph seam (#1503 Q2).The resource causes are split per-cause because a sizing operator grows a different seam for each; the malformed/opcode drops are fused into malformed_rx because they are one operator symptom (#1503 Q3).
Public Members
-
std::size_t flatten_dropped = 0¶
Frames dropped because the reject-reply flatten could not be served by the injected byte backend (
fwd_router.cpp, therejectarm).
-
std::size_t forward_iov_dropped = 0¶
Forward hops dropped because the scatter-gather iov table could not be built — the rope arm’s source refusing growth, or the contiguous arm overrunning
kFwdMaxIov. Both arms state one policy: drop, never truncate.
-
std::size_t arena_dropped = 0¶
Terminus requests dropped because the injected rx source could not serve the decode arena —
TLV_NESTING_TOO_DEEP, which RFC-0006 spells as “exceeds this
receiver’s decode resources”. The number a deployment grows
rxagainst. The request is still ANSWERED — an addressedSTATUS{BACKPRESSURE}built on the stack from its own bytes (#1612) — so this counts work lost, not silence.
-
std::size_t assemble_dropped = 0¶
Terminus REPLIES dropped because the resolver assembled an EMPTY rope — an allocation refusal inside reply assembly, surfaced as
link_count() == 0.
-
std::size_t reply_iov_dropped = 0¶
Terminus REPLIES dropped because the egress span table could not be drawn from the per-owner receive source (#1570). The request is lost; the client retries — at any reply size, including a composed root’s.
-
std::size_t delivery_iov_dropped = 0¶
Remote DELIVERIES dropped because the delivery’s head could not be built (
deliver_remote, full-route arm). Since RFC-0028 slice 9 the delivery draws no iov table (the head rides stack spans and the value goes to the link by reference), so this stays 0 unless a head outgrows its fixed stack buffer. A link that refuses the frame counts it in its owndropped_tx.
-
std::size_t malformed_rx = 0¶
Frames dropped as MALFORMED — a failed decode, a failed root CRC, an oversized or non-forwardable op, an unknown/unauthorized opcode. ONE bucket by ruling: an operator reads every one of these as the same symptom (“a peer is speaking
something this node cannot parse”), and nothing is sized against them.
-
std::size_t flatten_dropped = 0¶
-
class child_registry_t¶
This node’s
NAME → transport linktable (the compositor demux, ADR-0037).A child is addressed by its mount path —
/net/<module>/<name>(RFC-0014, ADR-0061): the run ofdstsegments this node consumes to route onward, and the run prepended tosrcon the way back. Lookups are lock-free.Link identity is the QUALIFIED name — one string, so every existing consumer of a link identity (route-handle label tables, subscriber-edge eviction, the departure notifiers) keeps working on an opaque string and needs no signature change. The demux, which holds two raw segment spans and must not allocate on the hot path (
bench_forward_heap’sallocs=0gate), matches through longest_prefix, which compares each slot’s key against thedstprefix in place and never builds a key.Shape is per-CONNECTION, not per-module — a refinement of ADR-0061, which assumed a module declares it. It cannot:
ws-server’speer_namedconfig decides whether the bus facet is exposed, so two connections in one module may differ. The shape is therefore captured ONCE at add time fromlink.bus()and stored on the slot, which still honours the ADR’s actual requirement — nobus()probe on the forward path. It is stored IN the link’s own word (#882, ADR-0063 erratum 6), so a reader can never pair one publication’s shape with another’s link.Mutation model (#494). The table was add-only, which left a retired link’s
name → transport_t*resident and dangling. erase closes that, and it does so by tombstoning in place — the slot’s link pointer is nulled and its NAME kept — never by erasing fromchildren_. That is deliberate: shifting the vector under a concurrent lock-free reader is a hard use-after-free, whereas a tombstone leaves the slot in place and a racing reader sees either the old pointer ornullptr. A later add of the SAME name reuses its tombstone, so create/remove churn on a stable name set does not grow the table; a genuinely new name still appends, so the table’s high -water mark is the count of DISTINCT names ever registered. Compaction (and the full mutation-vs-forward concurrency contract) lands with the RFC-0014 S5 liveness engine, where the TSan gate and a safe reclamation scheme arrive together — see ADR-0061 (docs/adr/0061-per-transport-mount-routing-strip-k-l5-demux.md).Slots ARE address-stable (#521, ADR-0063). The tombstone alone bought stability against ERASE only:
children_was astd::vector, so appending a genuinely new name reallocated and invalidated every slot reference and iterator in the table — which RFC-0014 turned from a dormant caveat into a live hazard by making connection create/remove a RUNTIME operation. The storage is now an append-only CHUNKED LIST: a chunk is never moved, resized, or freed before this object dies, so a slot’s address is fixed from the moment it is published. ADR-0062’s forward cache builds on exactly that — it holds aconst child_t*and reads the tombstone as its invalidation.Writers are serialized by the caller; readers are not (ADR-0063). add and erase are control-plane calls and must not run concurrently with each other —
add’s scan-then- append is not atomic, so two racing writers can be handed the SAME empty slot.fwd_router_tholds the lock that prevents this. Readers need no lock and take none.Public Functions
-
inline explicit child_registry_t(mem::block_source_t &src = mem::net_source()) noexcept¶
Draw this table’s chunks from
src(#873 phase 1).The chunks were
new (std::nothrow) chunk_t()on the global heap — one of the four channels #873 named, and the one an injected-everywhere node could still not bound. They are long-lived control state whose high-water mark is the count of DISTINCT link names ever registered, so a bounded node wants them inside its slab like everything else.fwd_router_tpoints this at itslabel_srcfor exactly that reason: same lifetime class as the label tables, and deliberately NOT the per-framerxstore, which a tr::mem::bump_source_t may legitimately be.The default is tr::mem::heap_source, and the process-default composition is byte-for-byte what it was:
heap_source_tserves a fundamental-alignment request through the same plain nothrowoperator newthenew (std::nothrow)expression used.- Parameters:
src – Must outlive this registry and every slot reference handed out of it.
-
inline ~child_registry_t()¶
Returns the chunks to the injected source. Nothing is reclaimed before this point, by design — see the class’s slot-stability contract.
-
inline bool add(std::string_view name, transport_t &link)¶
Register the link addressed by qualified name
name("<module>/<name>").REBINDS
name'sexisting slot when it has one — live or tombstoned — and only appends for a name the table has never held. Captures the link’s SHAPE here, once, so the forward path never probesbus(). A control-plane call: not for the forward path.A name has exactly one slot. Re-adding a LIVE name used to append a second, and that shadow slot reopened precisely the dangling-
transport_t*hole #494 closed: erase nulled only the first match and returnedtrue, so the caller destroyed its transport believing teardown had succeeded while by_name kept resolving the freed link through the shadow. Rebinding makes the name→slot mapping one-to-one, which is what every other operation here already assumes.[[nodiscard]]since #892. It was declined here on the ground that “the control-plane
tests that ignore it are unchanged, and the ONE caller that must not is `add_child`” — which is an argument for the attribute, not against it. Naming the one caller that must check is precisely what the compiler should be enforcing, and it was not:
add_childdiscarded this bool and reported success, somake_connectionminted a ghost UP-but- unroutable connection under heap exhaustion (#930). Tests that mean to ignore an allocation failure now write(void)and are unchanged in behaviour.A fresh slot copies
nameand its mount run into ONE block from the registry’s source (#1779); a refused block is the samefalsea refused chunk is.- Return values:
false – The table could not grow —
appendgot no chunk from the allocator, so NOTHING was registered. It used to returnvoidand swallow exactly this, leaving the caller (fwd_router_t::add_child) to report success and wire a receiver onto a link the registry does not hold: a GHOST child, audible on its transport, resolvable by nodst, and removable by noremove_child— the same “healthy-looking child that every forward misses” shape #523 was filed about.
-
inline std::uint32_t mount_generation() const noexcept¶
The MOUNT-SHAPE generation — bumped whenever a
dstprefix could start or stop resolving to a different mount (#765).The third validate-on-use stamp, beside
graph_t::retire_generation(a revived vertex) and the slot tombstone (a departed link). Neither of those two can see the hazard this one exists for: bind a label through mountnet/ws/s, then registernet/ws/s/rack, and a fullFWDresolves against the NEW, deeper mount while aCOMPACTriding the old label still dereferences the binding made against the old split. Both targets are alive and both are the vertex/link they always were — what moved is the POINT at which the address divides into “local mount” and “remote residual”.Until #523 the two planes agreed about a deeper mount only because NEITHER could reach it — the descent capped its width, so the deeper registration was unroutable to both. That is agreement by mutual failure, and lifting the width bound ends it.
Coarse ON PURPOSE: it counts mount-table mutations, not the mounts a given label depends on. A mutation that could not have changed one label’s split still restamps it, and that label takes the RFC-0004 §E.1 self-heal — drop, observe,
HANDLE_NACK, re-advertise. A per-label dependency set would be a reverse index, which is the option ADR-0062 already rejected: it moves work onto the control plane’s lock to serve the minority flow, and it is a SECOND invalidation mechanism beside one that works.
-
template<class SegAt>
inline const child_t *longest_prefix(SegAt &&at) const¶ The live child whose qualified name is the LONGEST prefix of the
dstsegmentsatyields — the forward demux’s entry point (#523).ONE pass over the table. Each slot is matched against the prefix of ITS OWN child_t::seg_count, so a mount of any width resolves and the descent never retries a width: O(N) slot visits, independent of how wide the widest mount is. It replaces a
k = W..1loop that re-scanned the whole table at every width — O(W×N), measured at 270 ns vs 25 ns for one scan at W=12, N=64 — and which, worse, could only ever match the widths a compile-time constant enumerated (kMountPeekMax, deleted with this).Segments are read through
atand compared against the stored key in place, so no key is ever built and the hot path stays allocation-free (bench_forward_heap’sallocs=0gate). Nothing here is sized by a width: there is no per-width array, no peek window, and no constant to raise.LONGEST-MATCH-WINS is the contract, preserved exactly: the old loop started at the widest key and returned the first hit, so a more specific mount always beat a shorter one. Here the incumbent’s width is the filter (
k <= best_kcannot win), which is also what keeps the pass cheap when a wide mount matches early.- Template Parameters:
SegAt – See prefix_walk_t.
- Returns:
The matched slot, or nullptr when no registered mount prefixes the
dst.
-
template<class SegAt>
inline const child_t *longest_prefix_confirmed(SegAt &at) const¶ longest_prefix with the confirm INSIDE the pass — the cold fallback.
Same contract, same answer, and it is the definition the fast path is an optimisation of. Reached only when the digest filter’s pick fails to confirm.
-
inline const child_t *longest_prefix(std::span<const std::string_view> segs) const¶
longest_prefix over a ready-made segment list — the control plane’s form.
on_advertiseholds decodedNAMEchildren, not a frame cursor, andsubscribe_towardholds a parsedpath_t. Same descent, same answer: the two planes resolving a mount by different rules is precisely what #516 was.
-
inline bool erase(std::string_view name)¶
Tombstone the link addressed by
name— it stops resolving.Call in the same step the link/vertex is torn down, and BEFORE the
transport_tis destroyed, so no forward can resolve a freed object. The slot itself is kept (see the class docs).- Returns:
true if
namenamed a live child, false if it named none.
-
inline const child_t *entry_by_name(std::string_view name) const¶
The link addressed by
name(nullptr if none).Resolution order (ADR-0044): an exact static child NAME wins; otherwise each registered BUS child (a link exposing transport_t::bus) is asked to resolve
nameas a currently-audible peer (bus_link_t::peer_link), yielding a DIRECTED per-peer endpoint. So an announced bus peer’s name is a routable next-hop segment with no registry mutation and no stored peer state — the peer table lives inside the bus transport and expires with its traffic.The live child slot registered under exactly
name(nullptr if none).Kept out of line and cold: on the forward path it is the no-ctx fallback only, and inlined it grew every
route_fwd_forwardinstantiation by its whole scan (#1779).
-
inline transport_t *by_name(std::string_view name) const¶
The link addressed by
name(nullptr if none), peer fallback included.The identity lookup used off the mount-descent path (reply/advertise plumbing, which addresses a link by its qualified name). Resolution order (ADR-0044): an exact child NAME wins; otherwise each registered BUS child is asked to resolve
nameas a currently-audible peer. Prefer longest_prefix on the forward path, and resolve_peer for scoped peer resolution.
-
inline std::size_t size() const noexcept¶
Number of slots — live children PLUS tombstones (test introspection).
-
inline std::size_t live_size() const noexcept¶
Number of children that still resolve (test introspection).
Public Static Functions
-
static inline constexpr std::size_t segment_count(std::string_view name) noexcept¶
Segments in a qualified mount name —
"a/b"is 2,""is 0.Counts separators rather than splitting: a count is all any caller wants, and building a vector of pieces to learn one would be the wrong shape even on a control-plane path.
-
static inline transport_t *resolve_peer(const child_t &child, std::string_view peer)¶
Resolve
peerwithin THIS endpoint’s own peer table (ADR-0061).The per-endpoint replacement for
by_name’s global cross-bus scan: a peer segment is resolved against the multi-peer child it was addressed through, so two servers’ same-named peers stay distinct and a peer is never reachable through the wrong module. A point-to-point child resolves no peer at all — and neither does anything, on a target that closed the bus module out (tr::net::bus_of, #375 deliverable 3).- Returns:
The directed per-peer endpoint, or nullptr if this child has no such peer.
-
static inline std::size_t encode_mount_name(std::string_view name, std::byte *out) noexcept¶
Encode qualified name
name("a/b") as a run of packed PATH segment records, one per segment ([u8 len][bytes], RFC-0018).Under the packed body there is only ONE form to emit — a record has no option byte — so the ADR-0062 §”Considered options” caveat this used to carry (emitting and MATCHING are different problems, because a peer may legally spell the same NAME with
opt.LL = 1) no longer applies: emitting and matching are now the same bytes. A segment that is empty (it would spell the §5.4 escape) or too long for the record’su8length field yields an empty run, and the hop falls back to encoding the name per-frame.One pass serves both halves of add’s single allocation (#1779): with
outnull it only measures, otherwise it writes the run there (sized by the measuring call). Public because it is a pure function ofname, and the router’s origin spells its own first-hop local part with exactly these bytes (fwd_router_t::adopt_path_label).- Returns:
The run’s byte count;
0⇔ the name has no packed spelling.
-
static inline constexpr std::uint64_t fold_segment(std::uint64_t h, std::string_view seg) noexcept¶
A per-segment digest of a qualified name — the scan’s cheap discriminator.
Deliberately NOT a general hash. A general hash (FNV-1a was tried) walks every byte in a serial xor-multiply chain, and on a short name that chain is ~24 dependent
imuls — MEASURED at ~30 ns, which is ~20% of a whole forward hop and is paid on EVERY lookup, including the single-child case that has nothing to scan. It made the fixed cost worse to make the scan cost better.This reads three cheap facts per segment — its length and its first and last byte — and folds them with one multiply per segment. For a two-segment mount that is ~3 multiplies instead of ~24, and it costs the same whether the names are 8 bytes or 80.
It is a FILTER, never a decision: a collision costs one full compare, which is what the scan did unconditionally before. The length pre-filter runs alongside it and catches a different axis, so the two together leave very little for the compare to reject.
-
static inline constexpr std::uint64_t digest_name(std::string_view name) noexcept¶
Digest a stored qualified name (
"<module>/<name>") by splitting it on/.Runs once per add — control plane — so the split costs nothing that matters. It MUST produce what digest_segments produces for the same name;
child_registry_testpins that agreement directly rather than only through a lookup, because a silent disagreement would not fail loudly: it would simply stop resolving.
-
static inline constexpr std::uint64_t digest_segments(std::span<const std::string_view> segs) noexcept¶
Digest
segs— the same value digest_name gives for them joined by/.
Public Static Attributes
-
static constexpr std::uintptr_t kBusShapeBit = 1¶
The SHAPE bit an egress word carries in the link pointer’s spare low bit.
Set ⇒ the link is MULTI-PEER (it exposes transport_t::bus). The shape is still captured once at add time, so the forward path still never probes
bus()— what changed (#882) is that it is captured IN the same word as the pointer it describes.
-
static constexpr std::uint64_t kDigestSeed = 0¶
The empty name’s digest.
-
struct child_slot_layout_oracle_t¶
child_t’s fields with child_t::owner_hint taken back out — the layout oracle for that word’s “costs the slot nothing” claim.
Spelled as a mirror rather than as an
offsetof, which is only conditionally supported on a type with mixed access control, and rather than as a literal== 80, which is a 64-bit host’s arithmetic and would break the 32-bit C6 build the whole per-slot byte argument is made for. What is pinned is the PROPERTY: the hint rides in paddingseg_countwas already leaving, so a future reordering that pushes it out of that hole — or a wider field that stops fitting — fails here instead of quietly costing the mount descent eight more cache lines per frame atN = 64.Public Members
-
std::uint32_t seg_count = 0¶
Mirrors child_t::seg_count.
-
std::uint64_t name_digest = 0¶
Mirrors child_t::name_digest.
-
std::string_view name¶
Mirrors child_t::name.
-
std::span<const std::byte> mount_tlv¶
Mirrors child_t::mount_tlv.
-
std::uint32_t seg_count = 0¶
-
struct child_t¶
One registered child: its qualified mount name, and its egress word.
nameis"<module>/<name>"(RFC-0014’s/net/<module>/<name>minus the constantnetroot). A null link marks a TOMBSTONE (#494) — the slot is dead but stays put so a concurrent lock-free reader’s iteration remains valid. The link and its shape are ONE atomic word and are read through egress; seeegress_for why.Aligned to two pointers so a 64-bit slot is exactly one cache line: with the name and mount run held as views (#1779) the natural size is 56 bytes, and that stride put one slot in eight’s
seg_count/name_digestpair across a line boundary on the descent’s hot scan, which measured as a +2% hop at 64 links. A 32-bit slot stays its natural size.Public Functions
-
inline egress_t egress() const noexcept¶
This slot’s link AND its shape, from ONE acquire load.
The only way to read either fact. A routing decision needs BOTH — the shape picks the branch, the link is what that branch sends over — and reading them as two loads let a rebind that FLIPS a name’s shape hand a forward one publication’s shape with another’s link (#882). The dangerous pairing is a stale point-to-point shape with a fresh BUS link: the descent then returns the bus link as a directed egress and its
send()fans out to every open peer, which is the one-request/N-replies misroute (#409) the descent’s rejected-hit branch exists to prevent. One word, one load, and that pairing cannot be spelled.
-
inline transport_t *link() const noexcept¶
This slot’s link alone (nullptr ⇒ TOMBSTONE) — for the shape-agnostic callers (identity lookups, teardown sweeps).
-
inline bool live() const noexcept¶
True while this slot still resolves — i.e. it is not a tombstone.
Public Members
-
std::uint32_t seg_count = 0¶
How many
/-separated segments name has — the slot’s OWN mount width.The field the INVERTED SINGLE-PASS descent turns on (#523). The descent used to try key widths
W..1and re-scan the whole table at each one — O(W×N) slot visits, which is invisible only while a constant caps W at 3. Storing each slot’s own width lets ONE pass match every slot against the prefix of exactly that width, so the descent is O(N) whatever the widths in the table are, and no width needs to be known in advance.Written once, on the append path, before the slot is published — the same contract name_digest has, and for the same reason: it is a pure function of name.
**Declared FIRST, beside name_digest.** These two are the ONLY fields the scan’s hot loop reads, once per slot; everything else is touched for the single slot that wins. Appended at the END of the struct they grew
child_tfrom 80 to 88 bytes, and 64 slots of that is eight extra cache lines the scan walks per frame. An earlier placement that tucked this field into the shape bool’s tail padding (offset 44, digest at 48) got the size back, but the PAIR then straddled a 64-byte boundary on one slot in every four. Here it is 16 bytes at offsets 0/8, so the size is 80 AND every slot’s hot read is one line. (The separate shape bool is gone since #882 — folding it intoegress_left both this offset and the 80-byte size unchanged.)Honest about what that last move bought: it was made to remove the straddle and the A/B moved by ~1 ns at
W = 3,N = 64— inside the run-to-run range, so it is a shape argument, not a measured win. It is kept because the straddle-free layout is the one that does not depend on the compiler having placed the fields luckily; the regression this PR actually had to close was code generation, not slot layout (seeresolve_mount_deepinfwd_router.cpp).
-
mutable std::atomic<std::uint32_t> owner_hint = {0}¶
An OPAQUE cache word the registry’s owner may keep per slot — free of charge.
The registry never reads it, never writes it and attaches no meaning to it. It exists because
seg_countabove already leaves four bytes of padding before name_digest’s eight-byte alignment, so a word parked here costs the slot nothing:child_tis 80 bytes with it and 80 bytes without, and the descent’s hot read is still the one line at offsets 0/8. A field appended at the end instead would have grown the slot to 88 and made the scan walk eight more cache lines per frame atN = 64— a frame-path cost for a control-plane convenience, which is the wrong way round.fwd_router_tkeeps this mount’sgraph_t::intern_link_hintedSLOT HINT here (#1437), so a mount-routed subscribe resolves the link’s interned token by subscript instead of by the graph’s linear name scan. That use imposes no invalidation contract on this struct and must not be given one: the hint is validated by NAME on every use, so a stale word, a rebound tenancy, a tombstone coming back to life and a fresh zero are all merely a wasted comparison. Anything stored here MUST have that property — the registry will not clear it for you, on add, on erase, or ever.Atomic and relaxed on both sides: a lock-free forward reader owns no part of this word, and the control plane is not publishing anything through it.
-
std::uint64_t name_digest = 0¶
A cheap digest of name, computed once at add time.
A pure function of the slot’s own name, so it has NO invalidation contract: a name has exactly one slot and add writes the name only on the append path, before the slot is published. Tombstoning nulls the link in
egress_and leaves this untouched, which is what lets the scan test it BEFORE the acquire-load — a stale-looking hash can only ever cause an extra live check, never a wrong answer.Why it exists: the scan’s per-candidate work was an acquire-load plus a string compare, so a wide table paid a real cost per frame even though at most one slot could match. An inline integer discriminator in the slot’s own cache line makes the overwhelming majority of candidates cost one compare.
-
std::string_view name¶
Qualified mount name,
"<module>/<name>"— a view of this slot’s own TEXT block, drawn from the registry’s source by add (#1779).The block holds the name followed by mount_tlv’s bytes, one allocation per slot, released only by the registry’s destructor. Immutable after publish for the reason mount_tlv gives, so a lock-free reader may hold either view.
-
std::span<const std::byte> mount_tlv¶
The mount path PRE-ENCODED as a run of NAME TLVs (#508), built once here so a forward hop emits the grown
srcprefix as ONE span with no per-segment work — and so no fixed buffer bounds how long a NAME may be.IMMUTABLE AFTER PUBLISH (ADR-0073’s sibling ruling on #684): the encoding is a pure function of the slot’s key (
encode_mount_name(name)), and a rebind matches by name — so a live slot’s replacement bytes are identical by construction, and add never reassigns them. That immutability is what lets the forward path read this vector as a span with NO lock while add runs concurrently on a control thread; reassigning here would be a use-after-free on the reader (#684). Slot reuse under a DIFFERENT name (should teardown ever recycle slots) inherits this invariant.A view into the slot’s text block, right after name’s bytes (#1779): empty when a segment has no packed spelling, and the hop then encodes the name per frame.
-
inline egress_t egress() const noexcept¶
-
struct egress_t¶
ONE read of a slot’s egress: WHERE it sends, and WHAT SHAPE that link is.
The two facts are only ever meaningful together — the shape decides which branch of the mount descent a link is routed down, so a reader that pairs one slot’s shape with another publication’s link routes a BUS link point-to-point and its
send()fans out to every open peer (#882, the #409 misroute). They are therefore read as a pair, by child_t::egress, and never field-by-field on any path.Public Members
-
transport_t *link = nullptr¶
The link; nullptr marks a TOMBSTONE (#494).
-
bool multi_peer = false¶
The shape published WITH that link.
-
transport_t *link = nullptr¶
-
template<class SegAt>
class prefix_walk_t¶ Walks a
dst’s leading segments once, forward, keeping the digest chain with it.The state the single-pass descent needs to test a slot of ANY width for one integer compare. fold_segment is an ACCUMULATOR —
h_k = fold(h_{k-1}, seg_k)— so the digests of every prefix form a chain, and reach walks it forward on demand: a table whose slots share a width folds it exactly once and every slot after the first costs two integer compares, the same per-slot work the old exact-match scan paid.- Template Parameters:
SegAt –
std::optional<std::string_view>(std::size_t)— segmentiof thedst,std::nulloptwhen thedsthas no segment there. An EMPTY string means “present but not routable” (over-long, or unreadable off a rope), which is a different answer: it stops the chain without meaning the address ended.
Public Functions
-
inline bool reach(std::size_t want)¶
Advance the chain to cover the first
wantsegments.- Returns:
false if the
dsthas no usable run that long — and then no LONGER run is usable either, whichlimit_remembers so the rest of the pass costs one compare per slot instead of one walk.
-
inline std::uint64_t digest() const noexcept¶
Digest of the first
reached segments — comparable toname_digest.
-
inline explicit child_registry_t(mem::block_source_t &src = mem::net_source()) noexcept¶
-
template<typename Fn>
class sink_slot_t¶ One
{function pointer, context}observer pair, published coherently.Holds a sink in the ADR-0047 hot-path shape — a plain function pointer plus an opaque
void*, never astd::function— and makes the pair readable from a transport receive thread while a control thread installs or clears it.Thread contract. set may race every reader. get returns a pair that was installed by ONE set call — never a new
fnbeside a stalectx— and the caller dispatches from that snapshot, so a concurrent clear can no longer null the pointer between a caller’s test and its call. Two set calls must NOT run concurrently with each other: the publish is serialized by the owner (fwd_router_tholds one mutex for its five slots), not by the slot, which is what keeps a slot three words wide.What a set costs a frame in flight. Installing or clearing leaves a window of a few instructions in which the slot reads as EMPTY, so a frame that lands exactly there is not dispatched to either the old sink or the new one. That is deliberate: these sinks are configured, not swapped per frame, and skipping one dispatch is strictly better than the alternative it replaces — calling the new
fnwith the oldctx, which every sink then casts.Cost: with no sink installed a read is ONE atomic load, which is what the plain member pair it replaces cost; with one installed it is three more loads of adjacent words and a compare. No lock, no spin, on either path.
The context pointer’s lifetime is the caller’s responsibility and must cover every possible dispatch, exactly as it must for
receiver_slot_t.- Template Parameters:
Fn – The sink’s function-pointer type; its first parameter is the context (e.g.
void (*)(void* ctx, std::uint16_t label)).
Public Functions
-
inline void set(Fn fn, void *ctx) noexcept¶
Install (or clear, with a null
fn) the sink.Safe to call while readers run; NOT safe to call concurrently with another set on the same slot — the owner serializes setters.
- Parameters:
fn – The sink;
ctxis passed back as its first argument.ctx – Caller-owned context; must outlive every possible dispatch.
-
inline bool installed() const noexcept¶
The ONE-load filter: false iff the slot is certainly empty.
Exposed for a caller whose read site is inside an
always_inlinebody it must keep small — the L4 per-edge dispatch loop (#1049) — so the cheap test can stay there while the coherent get moves into the out-of-line leg it guards. It is NOT a check-then-call licence: the guarded leg still dispatches from a get snapshot and does nothing when that snapshot is empty. Atruehere is only ever a hint, exactly as the identical load inside get is.
-
inline snapshot_t get() const noexcept¶
Read the pair coherently; dispatch from the result, never from the members.
- Returns:
The installed pair, or
{nullptr, nullptr}when no sink is installed or a set is in flight.
Terminus resolution¶
An inbound operation carries two claims, not one string: WHERE it arrived (the link name,
which a remote subscription’s deliveries route back through) and WHO sent it (the opaque
per-peer handle the transport minted at accept). The ACL subject is derived from the second
at the terminus, which is what makes a per-writer subject reachable at peer_named=false
— see ADR-0082 — the auth subject and peer_named are decoupled claims.
-
struct inbound_ref_t¶
The inbound identity of a resolved operation: WHERE it arrived and WHO sent it (#375 Part 2 / #1266, fused — the 2026-08-16 ruling on #375).
Two claims, and ADR-0082 is the reason they are two fields rather than one string:
link is ADDRESSING. It is this node’s NAME for the link (or bus peer) the request arrived over, and it is what a remote subscription’s deliveries route back through. It has always been the resolver’s
inbound_linkand its meaning is unchanged.peer is IDENTITY. It is the opaque per-peer handle the transport minted at accept (#1294) and the peer-receiver seam tags each frame with — eight bytes, register-passed, carried instead of re-supplying a name string per frame. The operation’s ACL SUBJECT is derived FROM it, at the terminus, through op_resolver_t::on_peer_subject.
Before the fusion these were the same
std::string_view, which is exactly what made a per-writer subject unreachable atpeer_named=false: one FLAT link has one name for every peer on it. Splitting them is what lets the subject differ per writer while the return route stays the link’s.The converting constructor from a
std::string_viewis deliberate and load-bearing for compatibility:resolve(fwd, "up")still means what it always did — no handle, so the subject IS the link name, byte for byte the pre-fusion behaviour.Public Functions
-
constexpr inbound_ref_t() noexcept = default¶
A resolve with no peer identity: the subject is the link name itself.
-
template<class S>
inline constexpr inbound_ref_t(const S &inbound_link) noexcept¶ Implicit from a bare link NAME — the pre-#375-Part-2 spelling, unchanged.
Templated over anything a
std::string_viewis constructible from, rather than taking one directly, because aresolve(fwd, "up")would otherwise need TWO user-defined conversions (const char[3]→string_view→ here) and stop compiling. The point of the implicit door is that no existing caller has to change; a door every literal misses is not that door.
-
inline constexpr inbound_ref_t(std::string_view inbound_link, net::peer_handle_t inbound_peer, const void *supplier_origin, const net::link_kind_t *inbound_kind = nullptr) noexcept¶
The full form the router builds: a link name, its frame’s peer, and the opaque token its subject supplier resolves that peer against.
Public Members
-
std::string_view link¶
This node’s NAME for the link the request arrived on; empty ⇒ LOCAL resolution (no remote-subscriber binding, the trusted local ACL door).
-
net::peer_handle_t peer¶
The interned per-peer identity of the writer (
tr::net::peer_handle_t, #1294). Notvalid()⇒ this kind supplied none, and the subject falls back to link.
-
const void *origin = nullptr¶
An OPAQUE token the subject supplier interprets — never dereferenced here.
A handle is meaningful only to the link that minted it, so resolving one to a subject needs to know WHICH link. The router passes its own per-child receive context through this field and reads it back inside its supplier; every other caller leaves it null. It is a
const void*becausetr::graphis L4 and may not name a transport type (core/STYLE.md — dependencies point up the layers only).
-
const net::link_kind_t *link_kind = nullptr¶
The transport-catalog
(kind, role)of the link the request arrived on, or null when it has none (#1650).Resolved ONCE per link, at registration, and carried here as the pointer the router interned — a request pays no lookup for it. A WRITE hands it to the target’s admission filter and handler as
write_ctx_t::link; no other operation reads it.
-
class op_resolver_t¶
Resolves an arena-decoded FWD against a local graph and builds the FWD{REPLY} rope.
Local-only (RFC-0004 / ADR-0035): no transport, no multi-hop forwarding, no route-handle. Construct over the node’s graph_t; call resolve once per inbound request FWD, with the arena from
wire::decode_into(ADR-0041).Public Types
-
using reverse_ref_fn_t = std::optional<wire::path_ref_element_t> (*)(void *ctx, std::string_view inbound_link)¶
The responder’s own reverse-direction element supplier (RFC-0024 §7.1 amendment 1): asked, at most once per mint-flagged remote subscribe, for THIS node’s reference to the connection vertex
inbound_linkarrived on.The mapping from a link NAME to its connection vertex is the transport plane’s (
fwd_router_t’s receiver contexts), which this graph-layer resolver deliberately cannot name — so it is injected as a bare{fn, ctx}pair, the ADR-0047 seam shape.nullopt(or no installed supplier — every pre-amendment embedder) means the responder cannot complete the reverse list: the subscription stores none and stays canonical-only, the documented degrade.
-
using subject_fn_t = std::string_view (*)(void *ctx, const inbound_ref_t &inbound, std::span<char> scratch)¶
The SUBJECT supplier (#375 Part 2 / #1266): asked, at most once per resolve and only for a request carrying a valid
inbound_ref_t::peer, for the ACL subject token that peer writes under.This is where *”the subject is derived at the terminus from the handle”* actually happens. The handle is opaque and the table that gives it meaning belongs to the transport plane, which this graph-layer resolver deliberately cannot name — so the derivation is injected as a bare
{fn, ctx}pair, the ADR-0047 seam shape on_reverse_ref and on_path_label already use.fwd_router_tinstalls one that forwards totransport_t::peer_subjecton the link the frame arrived over.Called at most ONCE per resolve, before any graph call, and never for a local resolve: the derived token is then the caller context for every gate the walk runs — the READ gate, the WRITE gate, the SUBSCRIBE gate and the
graph::write_ctx_ta HANDLER sees — so a handler and the:aclthat admitted its write cannot disagree.An EMPTY answer is the conformant default, not an error: the subject falls back to
inbound_ref_t::link, i.e. exactly the caller context the resolver used before the split. Every embedder with no installed supplier is that case, so no shipped deployment’s gate decisions move until a link starts minting per-peer subjects.- Param ctx:
The caller-owned context handed to on_peer_subject.
- Param inbound:
The request’s inbound identity —
peeris the handle to resolve andoriginis the opaque token the installer put there.- Param scratch:
At least
tr::net::kPeerNameCharsbytes of storage the token may be formatted into; it outlives the whole resolve, so the returned view may point into it.
-
using path_label_fn_t = std::span<const std::byte> (*)(void *ctx, std::string_view inbound_link, wire::path_ref_element_t target)¶
The TERMINUS’s path-label supplier (RFC-0027 §6.1 point 3): asked, on a successful operation only, for the label element standing for the residual this node just resolved.
§6.1 point 3 — *”the terminus does the same for the residual it resolved”* — is the residual half of the rewrite whose forwarding half rides
reply_label. The reply’ssrcIS the request’sdst(the residual), so the terminus’s rewrite is a SUBSTITUTION of that one region: the label REPLACES the string bytes and never appends, exactly as §6.1 requires in both directions, and it lands in the one region that survives to the origin (erratum 2).What is injected and why: a label is minted against a
peer_handle_tout of a table the TRANSPORT plane owns (tr::net::path_label_table_t), and this graph-layer resolver deliberately cannot name either. So the mapping from “the link this request arrived on” plus “the vertex it resolved to” to “seven encoded bytes” is injected as a bare{fn, ctx}pair — the ADR-0047 seam shapeon_reverse_refalready uses.The contract each side holds:
Post-auth, always (§8.1). The supplier is called only after the operation’s own gates have passed — after
graph_t::read/write/await/subscribe_wireanswered success — so no label is ever minted for a destination an ancestor ACL hides, and a denied operation answers denied and nothing else.An EMPTY answer is the conformant default, not an error (§6.3): the reply’s
srcstays the string it is today. Every host with no installed supplier is that case, so no shipped reply’s bytes move.The returned span must be exactly
tr::wire::kPathLabelRecordByteslong and must outlive the reply assembly; anything else is ignored and the part stays a string.
Public Functions
-
inline explicit op_resolver_t(graph_t &graph, mem::mem_backend_t *flat = &mem::heap_backend(), mem::mem_backend_t *egress = &mem::heap_backend(), mem::mem_backend_t *retained = nullptr) noexcept¶
Bind the resolver to the local
graphit resolvesdstagainst.The SPAN (arena) tier draws from it too since #801 — at one site,
arena_node::own_wire, the ADR-0041 §2 ownership copy of a borrowed arena span. That is the whole of what this tier allocates (itswire()/body()spans are borrowed from the frame and never materialize), and it was the last ownership copy in either tier still going toview::over_bytes’s global heap. It is the MCU terminus’s ORDINARY case, not an exotic one: a synchronous CAN/UART child delivers a contiguous span, so every WRITE it carries took the unbounded copy. A refusal is answered by value, through the empty-view BACKPRESSURE channel below — not throughspans_intact(), which stays a constanttrueon this tier because a borrowed span cannot be shortened by a refused allocation.The default is the global heap, so every existing call site is unchanged.
A refused flatten is answered BY VALUE, never by reading a short span: the resolve walk carries a per-call “spans intact” flag (
spans_intact()on the node-reader concept) and turns a refusal into an addressedkind=ERRORSTATUS{BACKPRESSURE}reply — or, when the refusal hit the reply’s OWN route bytes and no trustworthy address is left, into aBACKPRESSUREstatus on the error side, which the router drops. Never a truncated reply.An injected
flatMUST be thread-safe on the same termsfwd_router_tdocuments for its own: the terminus resolves on a transport child’s receive thread and several children receive concurrently. Must outlive the resolver.- Parameters:
graph – The node’s local graph.
flat – The byte backend the terminus draws its owned
segments from (#766, #793, #801) — the per-node contiguous-span materialize (view_node::ensure_cache, reached by everywire()/body()read of a multi-link TLV) and BOTH branches of the ownership copy (view_node::own_wire: the ADR-0053 ⑤ flatten of a straddling payload and the ADR-0041 §2 copy of a contiguous one — the latter reached the global heap throughview::over_bytesuntil #793, so which allocator a write used depended on where the PEER’s fragmentation happened to fall). These sit one call BELOWfwd_router_t::resolve_terminus_rope, and until #766 they took mem::heap_backend unconditionally: a bounded node that pointed every other injection at its own slab still drew from the global heap the moment a peer sent a FRAGMENTED terminus request — peer-drivable, and anabort()under-fno-exceptions.fwd_router_tpasses its ownflathere, so one injection now covers the router’s four sites AND the terminus.egress – The byte backend the FWD{REPLY}’s EGRESS-construction segments draw from (#795, ADR-0074) — the reply head (peer-driven size: the swapped route bytes plus the inline tail) and, on a mint, the trailing 12-byte
PATH_REF. It is the last reply-egress byte source a bounded node could not previously bound (both folded READs’ POINT-header framing — composed-root and":children"— is payload framing and draws from the graph’s value seam instead: #831, closed): the head was hard-wired toview::heap_alloc’s global heap regardless of every other injection. A DEDICATED seam, notflat:flatis documented and sized against FLATTEN (payload) bytes, and folding an egress head into it would silently re-scope a budget deployments already set. The default is the global heap, so every existing call site is byte-unchanged; a bounded node points it at its own slab and this allocation joins the bound. A refusal returns an empty rope thator_backpressureturns into an addressedkind=ERRORSTATUS{BACKPRESSURE}— exhaustion answered by value, never an abort. MUST be thread-safe on the same terms asflat. Must outlive the resolver.
-
result_t<view::rope_t> resolve(const wire::tlv_arena_t &fwd, const inbound_ref_t &inbound = {}, const view::view_t *frame_view = nullptr, const wire::path_ref_element_t *dst_label_target = nullptr, bool *deferred = nullptr)¶
Resolve an arena-decoded request FWD and build the zero-copy
FWD{REPLY}rope.The op-level outcome (NOT_FOUND for a non-local
dst, INVALID_PATH for a[*]wildcard on a non-subscriber path, TIMEOUT for an AWAIT, …) is encoded as akind=ERRORreply on the value side — a built reply, not a failure. The error side is reserved for a structurally malformed FWD (not a FWD, or missing the requiredop/dst/srcchildren) that no reply can describe, and for a REPLY frame (which is routed, not resolved, here).A request whose is a ZERO-LENGTH is UNACKNOWLEDGED (RFC-0004 Amendment 2, #1502): the return route is also the acknowledgement request, and the empty route is the request not made. A
WRITEis applied and answers with an empty rope — success, refusal and ACL denial alike, since none of them has anywhere to go. AREAD, anAWAIT, a mint-flagged frame or a:subscribers[]subscribe is MALFORMED on that route and refuses on the error side. The empty rope is therefore no longer only the egress-exhaustion signal it was; callers already treat it as “nothing to send” (fwd_router_t’s two terminus arms), and this is a second, deliberate reason for it.A non-empty
inbound.linkmakes an inbound:subscribers[]WRITE bind a REMOTE subscriber (#136): the slot retains this request’s accumulated return route (src, copied once — trailer-sliced) andinbound.link, so the producer fan-out delivers aFWD{WRITE}/ auto-promotedCOMPACTback over that link (RFC-0004 §D/§E.1). An emptyinbound.linkis the local-only field-write — sofwd_router_t, which knows the link, passes it; a bare local resolve does not.The operation’s ACL caller context (#81, ADR-0018) is the SUBJECT, which is derived HERE, at the terminus, and threaded through every graph call the walk makes — so with a subject resolver installed a denied op replies
kind=ERRORwithSTATUS{ERROR{VALUE tr::access::denied}}(0x0050). The derivation isinbound.peerthrough the installed supplier (on_peer_subject), falling back toinbound.linkwhen there is no valid handle, no supplier, or the supplier declines — which is every caller that passes a bare link name, and is byte for byte what the resolver did before the two claims were split.The arena (and the frame it borrows) only needs to outlive this call: every span the reply retains is copied once to its owner (ADR-0041 §2) — or, on an owning-delivery frame, referenced off it (ADR-0042 §3, below).
A non-null
frame_viewmarks the frame as OWNING (delivered as a refcountedview_tover the same bytes the arena borrows — the ADR-0042 receiver seam). Then a WRITE whose payload TLV (node.wire) is at least the target vertex’s copy-or-share threshold (RFC-0028 §5.3,graph_t::share_threshold_bytes) and whose opt byte carries no trailer bits is stored as a SUBVIEW of the frame — a refcount bump that holds the whole frame, zero copy. Smaller, trailered, or span-delivered payloads take the one-copy trailer-sliced store, into ONE inlinevalue_tblock (header plus bytes); the remote-subscriber return route always keeps its subscription-scoped one-copy behavior.A label REPLACES the string bytes of the part it stands for (§6.1), so a labelled
dstthat reaches its terminus carries no name to look up:path_lookup_keyrefuses an escape record in key context and must, because reading a peer’s slot index as UTF-8 is the guessing §7.2 forbids. The address is resolved BEFORE this call, by the transport plane that owns the label table (tr::net::path_label_table_t), and what arrives here is that resolution: this node’s own reference to the vertex the label aliases — the identical element RFC-0024’s bound spelling carries.Non-null means resolve against this element and not against ‘s bytes, on the SAME arm the bound spelling takes: one
deref_vertex_slot, then the operation’s own ACL gate insidegraph_t::read/write/awaitat the dereferenced vertex. That reuse is how §8.2’s *”exactly as the string form does”* becomes one implementation instead of two kept in agreement. A stale element — the vertex retired between the label’s mint and this frame — answersNOT_FOUNDon the error side, which the router turns into a drop, exactly as a stalePATH_REFdoes.Two MINTS are suppressed while it is set, and both are §11.2’s rule rather than an optimisation: this address is already spelled in one compressed form, so the reply neither mints a second label into it (there is nothing left to replace — the request’s
dst, which the reply echoes as itssrc, IS the label) nor answers an RFC-0024 mint request with aPATH_REFfor it (*”SHOULD NOT bind a
`PATH_REF` over a path whose elements are already labelled”*).
- Parameters:
fwd – An arena-decoded request FWD (from
wire::decode_into).inbound – WHERE the request arrived and WHO sent it (inbound_ref_t) — implicitly constructible from a bare link NAME, which is the pre-#375-Part-2 spelling and behaves identically. An empty
inbound.linkis a local resolution (no remote-subscriber binding); a validinbound.peeris what the installed subject supplier (on_peer_subject) derives the ACL subject from.frame_view – The owning frame view when the link delivers views (ADR-0042); nullptr on the borrowed-span path.
dst_label_target – RFC-0027 §7.2 at a terminus — the vertex a LABELLED
dstalready dereferenced to, or nullptr, which is every string- andPATH_REF-spelled request and therefore every request a host with no injected label table ever sees.deferred – Out-flag for a deferred AWAIT (ADR-0084). Non-null with a sink installed through on_await_defer, an AWAIT is handed to that sink,
*deferredis set, and the reply is an EMPTY rope that the caller does not send: the sink answers later. Null keeps the synchronous AWAIT.
- Returns:
The reply as a view::rope_t (head segment + roped payload views), or a
status_ton a malformed/non-request frame.
-
result_t<view::rope_t> resolve(const wire::tlv_view_t &fwd, const inbound_ref_t &inbound = {}, const view::view_t *frame_view = nullptr, const wire::path_ref_element_t *dst_label_target = nullptr, bool *deferred = nullptr)¶
Resolve a rope-delivered request FWD (the lazy
tlv_view_ttier) and build theFWD{REPLY}rope — the owning-delivery twin of the arena overload (ADR-0053 §7).The same terminus semantics as the wire::tlv_arena_t overload — it runs the ONE templated resolve walk, here over the forward-only wire::tlv_view_t reader (ADR-0053 §1), so a frame reassembled as a scatter-gather rope (fragmented WS / CAN) is resolved WITHOUT an interim flatten of the whole frame. Byte-identical replies to the arena tier for the same logical request (the differential oracle in
op_resolve_view_test).- Parameters:
fwd – A rope-backed request FWD (
wire::tlv_view_t::over).inbound – WHERE the request arrived and WHO sent it, exactly as the arena overload documents (inbound_ref_t).
frame_view – Reserved for the ADR-0042 owning-store seam; the rope tier stores its one ownership copy, so pass
nullptr.dst_label_target – The RFC-0027 §7.2 labelled-
dstresolution, with exactly the meaning and the two suppressed mints the arena overload documents at length. Both tiers take it because a labelled request may arrive fragmented like any other, and the two tiers answering one logical request differently is the drift ADR-0053 §7’s single walk exists to make impossible.deferred – The deferred-AWAIT out-flag, as the arena overload documents.
- Returns:
The reply as a view::rope_t, or a
status_ton a malformed/non-request frame.
-
inline void on_reverse_ref(reverse_ref_fn_t fn, void *ctx) noexcept¶
Install the reverse-element supplier (null
fnuninstalls).
-
inline void on_peer_subject(subject_fn_t fn, void *ctx) noexcept¶
Install the subject supplier (null
fnuninstalls).
-
inline void on_path_label(path_label_fn_t fn, void *ctx) noexcept¶
Install the terminus path-label supplier (null
fnuninstalls).
-
inline void on_link_id(link_id_fn_t fn, void *ctx) noexcept¶
Install the terminus LINK-TOKEN supplier (null
fnuninstalls) — #1266 / #1417’s carry.The seam that lets a remote subscribe reach the subscriber index by SUBSCRIPT instead of by name hash. The supplier is the transport plane’s: it minted the link’s
graph_t::intern_linktoken when the link (or the bus peer) became audible and cached it in its own per-link receive context, which is what inbound_ref_t::origin points at. Worth 42–56 % of the index operation net of its control, and 27 % of its bytes.ASKED LAZILY, at the subscribe branch and nowhere else. That is the load-bearing part of the contract: a control-plane saving paid on every terminus frame is what killed #1290’s prototype, so unlike subject_fn_t this one is NOT resolved once per resolve. A read, a write, an await and a forwarding hop never call it.
An invalid answer is the conformant default and costs nothing but the lookup the index does today — no installed supplier, a peer the supplier has no token for, a census bus that never announced. So is a WRONG answer: the index verifies that the token’s slot spells the key it is about to index under, so a supplier that confuses two links loses a subscript, not an edge.
-
inline void on_await_defer(await_defer_fn_t fn, void *ctx) noexcept¶
Install the AWAIT deferral sink (ADR-0084);
nullptruninstalls it.With a sink installed, a resolve given a
deferredout-flag no longer runsgraph_t::awaiton the calling thread: it gates the READ, hands the request to the sink and returns. The router installs it so a remote AWAIT never holds the receive context of the link it arrived on.- Parameters:
fn – The sink;
ctxis caller-owned and must outlive every resolve.ctx – Handed back to
fn.
-
using reverse_ref_fn_t = std::optional<wire::path_ref_element_t> (*)(void *ctx, std::string_view inbound_link)¶
-
enum class tr::graph::fwd_op_t : std::uint8_t¶
The four FWD operations (RFC-0004 §B — the
opchild, a u8).Values:
-
enumerator READ¶
Read the data LKV or the selected
:field.
-
enumerator WRITE¶
Write the payload TLV to the vertex or the selected
:field.
-
enumerator AWAIT¶
Block for the next write (honoring
await_timeout).
-
enumerator REPLY¶
A reply routed back; not resolvable as a request here.
-
enumerator READ¶
Delivery compaction¶
-
class route_handle_t¶
Per-connection
label ↔ routetables for ws delivery-compaction (RFC-0004 §E.1).The label state lives PER LINK (ADR-0038 §3 / ADR-0039): each connection owns its own small tables — an INGRESS table (a label arriving on the link → its handle_binding_t), an EGRESS table (a label this node advertised over the link → the route it aliases, retained so a NACK can re-advertise), and a monotonic label allocator — drawn from the injected tr::mem::block_source_t and guarded by the LINK’S OWN mutex, so label traffic on one connection never contends with another. The only cross-link lock is a
shared_mutexover the link registry, taken exclusively when a link’s tables are first CREATED or when clear_link removes them. Each link’s tables are REFCOUNTED, so the accessors hand out a PINNING copy:clear_linkcan erase the registry entry while a concurrent writer still holds the tables — the node is destroyed only when the last outstanding reference drops (no dangling reference), and the registry is bounded to LIVE link names instead of growing one empty shell per departed name (#488). State exists only for flows that opted into compaction, so ingress_count on a node forwarding only one-shot/cold traffic is zero.Public Functions
-
inline explicit route_handle_t(mem::block_source_t *src = &mem::net_source(), std::size_t max_bindings_per_link = 0)¶
Draw all label state from
src(ADR-0065 / ADR-0079), bounding each link’s tables atmax_bindings_per_linkentries.A bounded node passes a
mem::pool_source_tover its slab and the label tables live entirely in host-chosen memory; the default is the process-wide nothrow platform heap.srcmust outlive this object, and must be thread-safe if more than one transport receive thread can reach this store (the RFC-0014 wire-driven paths do).The bound is injected, never assumed ([CONTEXT.md §Resource bound]).
0means unbounded — the default, and the pre-#603 behavior. A bounded host sizes it from its own slab; ADR-0038 §3 calls for exactly this (*”sized by `:settings`”*).Since #603 defect 1 the SOURCE is a bound in its own right, which is ADR-0079’s “a bounded node is a property the deployer injects”: a full source refuses exactly as a full table does, so a deployment can size the label plane by the slab alone and leave
max_bindings_per_linkat0. The two are complementary rather than redundant — the count bounds ONE link’s share, the slab bounds the node’s total.- Parameters:
src – Where all label state is allocated (nothrow, by value).
max_bindings_per_link – Ceiling on a link’s ingress table AND, separately, its egress table;
0⇒ unbounded.
-
~route_handle_t()¶
Release every live link’s tables (and the byte blocks they own).
-
bool bind_ingress(std::string_view in_link, std::uint16_t label, handle_binding_t binding)¶
Record an ingress binding: a
labelarriving onin_linkmeansbinding.Rebinding a label already present always succeeds — it replaces in place and adds no entry. Only a NEW label can be refused, and only when the link is at
max_bindings_per_link.- Parameters:
in_link – This node’s NAME for the link the ADVERTISE/COMPACT arrives on.
label – The label as seen on that inbound link.
binding – Its meaning (forward-swap or local terminus). Its
down_link/local_routebytes are BORROWED for the call and copied into the store’s own blocks; nothing is retained.
- Return values:
false – The link’s ingress table is full, or the injected source is exhausted — nothing was recorded, and refused_bindings
was incremented. A COMPACT on the unbound label then takes the same drop-and-HANDLE_NACK path a stale label already takes, which prompts the peer to re-advertise. The two refusals are deliberately ONE answer: ADR-0079 makes the injected store’s size a bound, so “the slab said no” degrades exactly as “the count
said no” already did, and no caller learns a new shape.
-
std::uint32_t link_epoch(std::string_view link) const¶
The CLEAR EPOCH of
link— the token that says “this link’s tables have not been
reconnected since” (#827).
Sampled by a forwarding hop BEFORE it mints anything against its downstream link, and handed back to bind_ingress_forward when the swap is finally bound. Every clear_link advances a node-wide counter, and a link’s tables carry the value they were created at, so once the link HAS tables its epoch changes only when THIS link is cleared — an unrelated link’s reconnect leaves it alone. Before the tables exist there is nothing to stamp, so a sample reads the shared counter: an unrelated clear_link landing between that sample and the table creation makes the fresh tables stamp the bumped value and the bind is refused SPURIOUSLY. The refusal direction is safe — the upstream’s next COMPACT draws a stale-label NACK and the re-advertise binds normally — a liveness nit confined to a link’s first-contact window, never a stale binding.
- Parameters:
link – This node’s NAME for the link.
- Returns:
An opaque token, meaningful only when compared against a later sample of the SAME link. A link with no tables reads the current counter, so creating them changes nothing and the first advertise on a link is refused only in the first-contact window above.
-
bool bind_ingress_forward(std::string_view in_link, std::uint16_t label, handle_binding_t binding, std::uint32_t down_epoch)¶
bind_ingress for a FORWARDING swap, refused if
down_epochwent stale.The store-under-inbound / point-at-outbound asymmetry again (#716), now in its racing form (#827). clear_link’s cross-link sweep can only erase bindings that already exist; an
on_advertiserunning on another link’s rx thread mints its out-label and retains its egress route against the PRE-clear downstream table, and binds the swap afterwards. A reconnect landing between those two steps sweeps the inbound link before the binding is there, and the binding lands after the table it aims into is gone — reproducing the exact #716 state the sweep exists to prevent, through a window measured in microseconds and with a permanent outcome.So the swap is bound only if
down_epoch— sampled from link_epoch before the mint — still names the downstream tables the label was minted against. The epoch read and the insert are ONE critical section against clear_link, so the sweep cannot interleave between them. Refusing takes the path a full ingress table already takes (false, nothing recorded): the peer’s COMPACT misses, draws the ordinary stale-label HANDLE_NACK, and the flow re-advertises from a clean slate. Nothing new goes on the wire, and this is the COLD advertise path — the per-delivery path is untouched.A refusal is deliberately NOT counted in refused_bindings
, which means “a link’s
table was at its injected bound”. This is not a resource refusal and it is not silent: the very next COMPACT on the unbound label fires the stale-label observer, which is where the event is already visible.
- Parameters:
in_link – This node’s NAME for the link the ADVERTISE arrived on.
label – The label as seen on that inbound link.
binding – The forwarding swap;
binding.down_linknames the linkdown_epochwas sampled from. A terminus binding has no downstream half and must use bind_ingress instead.down_epoch – The link_epoch of
binding.down_link, sampled BEFORE the out-label was minted.
- Return values:
false – Nothing was recorded — either the ingress table is at its bound, or the downstream link was reconnected inside the window.
-
binding_copy_t copy_binding(std::string_view in_link, std::uint16_t label, std::span<char> link_out, std::span<std::byte> route_out) const¶
Copy what a
labelarriving onin_linkmeans into CALLER storage — allocation-free (found == false⇒ stale/unknown).Replaces the owning
lookup_ingress(#603 defect 1). That returned astd::optional<handle_binding_t>whosestd::string+std::vectorwere built on the throwing global heap — on the COLD COMPACT arm, which a peer provokes by sending a frame on a label whose resolution has not been memoized yet, and on the observed-delivery arm. Both are peer-driven, so both wereabort()candidates on-fno-exceptions. There is no allocation here at all: the bytes land inlink_out/route_out.A buffer that is too small does NOT lose the answer. The binding’s real sizes are reported and
truncatedis set, so a caller can retry against a block grown from its own injected source (fwd_router.cppdoes exactly that for a route wider than its frame buffer). That is the difference between “the route does not fit here” and “there is no
binding”, which a plain empty result would have conflated.
- Parameters:
in_link – This node’s NAME for the inbound link.
label – The inbound label.
link_out – Destination for a FORWARD binding’s downstream link name. A link name is one path segment, so
graph::kMaxSegmentBytesalways suffices.route_out – Destination for a TERMINUS binding’s local route bytes.
- Returns:
The binding’s facts, with views into
link_out/route_outthat are valid only while those buffers are.found == false⇒ drop + NACK, exactly as anulloptdid.
-
resolved_binding_t resolved(std::string_view in_link, std::uint16_t label) const¶
The steady-state lookup: the RESOLVED binding, by value, allocation-free.
Prefer this on the COMPACT hot path. The retired owning
lookup_ingresscopied astd::stringand astd::vectorout of the table on every call — two allocations per frame, paid before anything checked whether the flow was already resolved, and both on the throwing global heap. This copies ~24 trivially copyable bytes instead, and returnsfound=falsefor an unknown label so the caller still NACKs identically.A
warm=falseresult means the caller must resolve and then call cache_resolution.
-
std::size_t copy_local_route(std::string_view in_link, std::uint16_t label, std::span<std::byte> out) const¶
Copy a TERMINUS binding’s local route bytes out, without allocating.
The warm-COMPACT companion to resolved.
resolved_binding_tdeliberately omits the route bytes because a warm delivery does not need them to WRITE — but an installed fwd_router_t::on_compact_delivery observer is handed them, and serving it through the retired owninglookup_ingressmade the warm path re-pay the whole owning copy (astd::stringplus astd::vector) that resolved exists to remove, per frame.The bytes are copied under the link’s own mutex into caller storage rather than handed to a callback from under it: the observer is host code, and the lock order this class establishes is registry → table, so invoking anything re-entrant while holding a table mutex would invert it against clear_link.
- Parameters:
in_link – This node’s NAME for the inbound link.
label – The inbound label.
out – Destination; written only when the route FITS.
- Returns:
The route’s full size in bytes —
0when no binding exists (or it is a forwarding swap, which has no local route). A return greater thanout.size()means nothing was written and the route is too long forout; the caller retries against a larger buffer (copy_binding reports the same size). Callers must therefore test the returned size againstout's, never assume a copy.
-
void cache_resolution(std::string_view in_link, std::uint16_t label, const resolved_binding_t &r)¶
Record the resolution
ragainst (in_link,label) so later frames skip it.Idempotent and best-effort: a binding that vanished between resolve and cache is simply not updated, and the next frame resolves again. Never invalidates a live delivery.
-
bool record_egress(std::string_view out_link, std::uint16_t label, std::span<const std::byte> route)¶
Remember the
routeadvertised overout_linkunderlabel.Lets this node re-advertise the binding on a HANDLE_NACK (or reconnect) without re-deriving the route. Idempotent — re-recording the same key replaces it.
- Parameters:
out_link – This node’s NAME for the downstream link the ADVERTISE went out on.
label – The label this node assigned for that downstream flow.
route – The (possibly stripped) dst PATH TLV bytes the label aliases. Borrowed for the call and copied into the store’s own block.
- Return values:
false – The link’s egress table is full, or the injected source is exhausted — nothing was recorded and refused_bindings was incremented. The caller must not advertise a label it cannot re-advertise on a NACK.
-
std::pair<std::uint16_t, bool> ensure_egress(std::string_view out_link, std::span<const std::byte> route)¶
Find this link’s label for
route, or allocate + record a fresh one (#136).The producer-origin lazy-advertise primitive (RFC-0004 §E.1, Q5): the first compact delivery on a
(out_link, route)flow has no binding, so a new label is allocated, recorded as egress, and returned withfresh == true(the caller must send the ADVERTISE once); subsequent deliveries find the same label and returnfresh == false(send only the COMPACT). clear_link drops the binding so a post-reconnect delivery re-advertises — the self-heal, with no transport “up” event. Since #913 the forwarding-hop swap andfwd_router_t::advertisemint here too, in place of an alloc_label + record_egress pair that minted unconditionally and burned one label per re-advertise cycle.A fresh entry is born RECLAIMABLE and the first reuse of it clears that (#833): while the mint is the only take of a label, the minter may still hand it back with release_egress. The steady-state delivery path pays one byte-load and a not-taken branch for that — the store happens at most once per entry, on the first reuse — and never a store on an entry a second caller has already taken.
- Parameters:
out_link – This node’s NAME for the downstream link.
route – A complete PATH TLV’s bytes — the delivery route the label aliases.
- Returns:
{label, fresh}— the (reused or new) label, and whether it was just created.{0, false}⇒ no label is available: this link’s label space is exhausted (see alloc_label), its egress table is atmax_bindings_per_link, or the injected source could not serve the route copy. Nothing was recorded; the caller delivers over the full-route FWD path. A flow ALREADY in the table is never refused — reuse is checked before the bound, so a full table degrades new flows only.
-
void release_egress(std::string_view out_link, std::uint16_t label, std::span<const std::byte> route)¶
Hand back a label + egress route taken from ensure_egress and never put on the wire — the refused-bind unwind (#833).
A forwarding hop mints its out-label and retains its stripped egress route BEFORE it binds the ingress swap, because the binding names the label. When that bind refuses — a full ingress table, or the #827 epoch guard — the hop returns without advertising, so the label it minted aliases a route no ingress binding aims at and no peer has ever seen. Nothing reclaimed it short of the downstream link’s next clear_link. This gives it back: the entry is erased, and the label itself returns to the allocator when it is still the most recently minted one.
Only the MINT is reclaimable.
Since #913 an egress entry is SHARED — one label serves every ingress flow whose stripped route is identical — so erasing it on one claimant’s refusal would strand every other. The entry therefore carries “the mint is
still the only take of this label”, set when
ensure_egress creates it and cleared by the first reuse, and this call erases nothing once that is false. That is what makes the two-thread interleaving safe without holding a lock across the bind: an advertise on another link’s rx thread that reuses the label between this caller’s mint and its refusal has already cleared the flag, so the entry it now depends on survives. An established flow is untouched by construction — its take was a reuse.Note
A no-op when the link has no tables — a release must not CREATE a link shell, which is the state link_count bounds (#488). So the common companion of a refusal, a downstream reconnect that erased the whole table, costs nothing here.
- Parameters:
out_link – This node’s NAME for the downstream link the label was minted on.
label – The label ensure_egress returned.
0is ignored.route – The route bytes that were passed to ensure_egress; an entry whose route has since been replaced (record_egress) is left alone.
-
std::size_t copy_egress_route(std::string_view out_link, std::uint16_t label, std::span<std::byte> out) const¶
Copy the route this node advertised over
out_linkunderlabelintoout— allocation-free (for re-advertise).Replaces the owning
egress_route(#603 defect 1). That one was already nothrow, but it got there by probing the GLOBAL heap throughdetail::try_assign— which on-fno-exceptionsfrees the probe block and then runs a throwingassignon the inference that the block is still free. This arm is reached from an inbound HANDLE_NACK on a transport receive thread, so a racer in that window is the normal case, and theassignhitting exhaustion inside anoexceptaborts the node (#850, the #981 residual). Copying into caller storage removes the probe, the window and the global-heap reach in one move.- Parameters:
out_link – This node’s NAME for the downstream link.
label – The downstream label.
out – Destination; written only when the route FITS.
- Returns:
The route’s full size in bytes —
0when no route is bound for this label. A return greater thanout.size()means nothing was written; the caller retries against a larger buffer (its own injected source) rather than reading a truncated route. Callers MUST test the returned size againstout's.
-
std::uint16_t alloc_label(std::string_view link)¶
Allocate a fresh, per-link, monotonic label (≥1; 0 is reserved “none”).
The allocator SATURATES rather than wrapping (#603). It issues 1..65535 in order and is then permanently exhausted, returning 0 — because a wrapped counter re-issues labels that still alias live routes, and a delivery on a reused label resolves the WRONG route (a misroute, not a drop). Labels are not reclaimed individually today; clear_link drops a link’s whole table and restores its space, which is the self-heal a (re)connect already performs.
- Parameters:
link – This node’s NAME for the link the label is scoped to.
- Returns:
A label unique among this link’s currently allocated labels, or 0 when the link’s 16-bit space is exhausted — callers MUST treat 0 as “cannot compact” and fall back to the full-route form, never stamp it on a frame.
-
void clear_link(std::string_view link)¶
Drop ALL state (ingress, egress, allocator) for
link, AND every ingress binding on any OTHER link whose downstream half crossedlink— the self-heal hook.A transport calls this on (re)connect/disconnect of
linkso a subsequent re-advertise rebinds from a clean slate; a delivery on a now-cleared label is stale and is NACK’d rather than misrouted.The cross-link sweep is what makes that true on a MID-CHAIN node (#716). A forwarding binding is stored under the inbound link while
handle_binding_t::down_linknames the outbound one, so clearing onlylink'sown tables leaves an ingress binding elsewhere still pointing at an out-label that died with them. The upstream never saw the reconnect and so never re-advertises: it keeps streaming COMPACTs, this node keeps forwarding the dead out-label, the downstream keeps NACKing, and the NACK is answered from the very table that was erased — a permanent, silent drop of the whole flow. Erasing those bindings makes the upstream’s next COMPACT miss, which draws the ordinary stale-labelHANDLE_NACKand prompts the upstream to re-advertise; nothing new is put on the wire. Terminus bindings have no downstream half and are never swept.The sweep can only erase bindings that already EXIST, which is why it is only half the guard (#827): a forwarding swap still in flight on another rx thread is bound after the sweep has already scanned its inbound link. That half is bind_ingress_forward’s — this call advances the clear epoch link_epoch reports, so a swap minted against the tables erased here is refused rather than bound.
Cost is O(links x bindings) on this COLD (re)connect path; the per-delivery path is untouched.
- Parameters:
link – This node’s NAME for the link whose state to forget.
-
std::size_t ingress_count() const¶
Count of live ingress bindings (tests assert a non-compact flow holds 0).
-
std::size_t egress_count() const¶
Count of live egress (advertised) bindings.
-
std::size_t link_count() const¶
Count of live per-link table shells in the registry (diagnostic).
One shell per link name that currently holds compaction state. clear_link reclaims a departed link’s shell, so a workload that churns through many distinct link names returns here to its steady-state live-name count rather than growing unboundedly (#488). Tests assert this reclamation.
-
inline std::size_t refused_bindings() const noexcept¶
Count of bindings refused because a link’s table was at its bound, or because the injected source was exhausted (diagnostic).
ONE counter for both, because ADR-0079 makes them one fact: the store’s size IS a bound, so “the slab refused” and “the count refused” are the same operator-visible event — this node is delivering some flows over the full-route form. Splitting them would ask an operator to watch two counters for one symptom.
The counted-drop half of the bounded-resource contract (
can_reassembly_tkeepsdropped_groupsfor the same reason): a bound that silently discards work is indistinguishable from one that is never reached. A non-zero value here means some flows on this node are delivering over the full-route form rather than compacted — degraded throughput, never a wrong or missing delivery.
-
std::size_t labels_used(std::string_view link) const¶
Labels SPENT out of
link's16-bit space — used-polarity occupancy of the one resource this class cannot grow (#1503 finding 3).capacityfor this seam is the constant 65535 (see alloc_label): the space is per-link and fixed by the wire, so unlike every other bounded resource in the tree there is no injected ceiling to report beside it. Free is65535 - labels_used(link).The allocator is monotonic and saturating — labels are not reclaimed individually, so this only ever rises until clear_link forgets the link and restores its whole space. A value climbing toward 65535 on a long-lived link is the advance warning for labels_exhausted; reading it after the fact only tells you the degrade already happened.
- Parameters:
link – This node’s NAME for the link.
- Returns:
Labels issued on
link,0for a link that holds no compaction state, and 65535 once the space is spent.
-
inline std::size_t labels_exhausted() const noexcept¶
Times a mint was refused because a link’s 16-bit label space was SPENT (#1503 finding 3) — the previously silent degrade.
Distinct from refused_bindings, and deliberately not fused into it: that counter means “a link’s table was at its INJECTED bound”, which a deployment answers by sizing the table up. This one means the wire’s own 65535/link space ran out, which no amount of memory fixes — the answer is a reconnect (clear_link) or fewer distinct flows. One event, one counter, but only where an operator would act differently.
The degrade it makes visible is the one #1491 showed matters: a caller handed
0falls back to the full-route form, which REPLIES per frame — so throughput drops and the reply traffic returns, with nothing on the wire saying why.Counted, never enforced (
core/STYLE.md§Introspection): the library does not refuse or reconnect on its own account.
-
inline explicit route_handle_t(mem::block_source_t *src = &mem::net_source(), std::size_t max_bindings_per_link = 0)¶
-
struct resolved_binding_t¶
The RESOLVED form of a binding — everything a steady-state COMPACT frame needs.
ADR-0062’s point, made concrete: an established flow must not re-derive what it already knows. This is trivially copyable and allocation-free, so route_handle_t::resolved can hand it out from under the link’s mutex without the
std::string+std::vectorcopylookup_ingresspays on EVERY frame today — a cost that landed before anything even looked at whether a resolution was cached.The route bytes are deliberately absent. A warm binding never touches them: the terminus dereferences
targetand writes; the forwarding hop sends ondown. They are needed only to RE-resolve, which is the cold path and keeps the owning accessor.Both cached forms are self-invalidating rather than notified — no callback fires from under a lock:
targetis paired withtarget_gen, compared againstgraph_t::retire_generation; a retired-and-revived vertex bumps it (#511), so a stale handle is detected on use (RFC-0009 §B.6 re-virginize).downis read from the registry SLOT, whoselinkteardown nulls in place (ADR-0063 made slot addresses permanently stable) — so a departed link readsnullptr: the same clean miss as an unresolved lookup. The tombstone IS the invalidation.
Public Members
-
bool found = false¶
false ⇒ no binding for this (link, label).
-
bool terminus = false¶
true ⇒ deliver locally; false ⇒ forward + swap.
-
bool warm = false¶
true ⇒ the resolution below is populated.
-
std::uint16_t out_label = 0¶
Forward: label to stamp downstream.
-
const void *down_slot = nullptr¶
Forward: cached registry slot.
-
std::optional<graph::vertex_handle_t> target¶
Terminus: the cached vertex.
std::optionalrather than a defaulted handle — ADR-0056 keepsvertex_handle_topaque and ALWAYS valid, so “no resolution yet” must be expressed outside the handle, not as an invalid one.
-
std::uint32_t target_gen = 0¶
Terminus: generation
targetwas resolved at.
-
std::uint32_t mount_gen = 0¶
The MOUNT-SHAPE generation this binding was resolved against (#765) — see handle_binding_t::mount_gen. Carried into the allocation-free view so the warm COMPACT path can validate it without taking the owning form.
-
struct handle_binding_t¶
One learned per-link label binding — what an inbound label means here.
Either a forwarding swap (rewrite to
out_labeland re-emit overdown_link) or a local terminus (resolvelocal_routeand apply the write). The trailing fields memoize what that resolution produced (ADR-0062), so an established flow stops re-deriving it; the allocation-free view of them is resolved_binding_t.A NON-OWNING descriptor (#603 defect 1, ADR-0079). It used to carry a
std::stringand astd::vector, which made it the structural reason this store could not leavestd::pmr: an entry holding those types is neither trivially copyable nor trivially destructible, somem::block_array_tcould not hold it and the tables stayed on a throwing allocator that a peer’s ADVERTISE reaches. The bytes are now the CALLER’s, borrowed for the duration of the route_handle_t::bind_ingress call, and the store copies them into blocks drawn from its injected tr::mem::block_source_t. That inverts the ownership so both halves get what they need: the caller can point at a decoded frame it already holds (no allocation at all), and the store’s copy fails by value instead of throwing.Warning
The views must outlive only the bind call. Nothing here is retained.
Public Members
-
bool terminus = false¶
true ⇒ deliver locally; false ⇒ forward + swap.
-
std::string_view down_link¶
Forward: this node’s NAME for the downstream link.
-
std::uint16_t out_label = {}¶
Forward: label to stamp on the downstream COMPACT.
-
std::span<const std::byte> local_route¶
Terminus: the local dst PATH TLV bytes to resolve + write.
-
bool warm = false¶
ADR-0062: the resolution, filled on first use and re-filled when it goes stale.
warm == falsemeans “never resolved”; the two cached forms carry their own staleness signal (see resolved_binding_t).true ⇒ the cached fields below are filled.
-
const void *down_slot = nullptr¶
Forward: cached
child_registry_t::child_t*.
-
std::optional<graph::vertex_handle_t> target¶
Terminus: the cached vertex.
-
std::uint32_t target_gen = 0¶
Terminus: generation
targetwas resolved at.
-
std::uint32_t mount_gen = 0¶
The mount-shape generation this binding’s SPLIT was decided against (#765).
The THIRD validate-on-use stamp, and it exists because the other two cannot see this hazard.
target_gencatches a retired-and-revived vertex; the slot tombstone catches a departed link. Neither catches the split MOVING: bind a label through mountnet/ws/s, then registernet/ws/s/rack, and a fullFWDresolves against the new, deeper mount while aCOMPACTriding this label still dereferences the binding made against the old one. Both targets are alive and both are what they always were — what changed is the point at which the address divides into “local mount” and “remote residual”, and the two planes now disagree about the same address with nothing reporting it.Until #523 the disagreement was unreachable, but only by accident: the descent capped its width, so a deeper mount was unroutable to BOTH planes. That is agreement by mutual failure, and it stopped holding the moment the width bound was lifted.
Compared against child_registry_t::mount_generation on use; a mismatch takes the SAME RFC-0004 §E.1 self-heal a stale label already takes — drop, fire the stale-label observer,
HANDLE_NACKupstream to prompt a re-advertise. No new error code, and no second invalidation mechanism (the objection that killed ADR-0062’s reverse index).
-
bool terminus = false¶
The FWD frame view¶
-
struct fwd_hdr_t¶
One top-level TLV header read in isolation (NO descent) — the byte offsets the zero-copy forward rebuild needs.
Kept as ABSOLUTE offsets into the source so the rebuild can re-slice src/payload as views (no copy). It is a thin ADAPTER over the ONE wire grammar (
grammar::parse_header, ADR-0048 §1): the length math is not mirrored here — this only turns the grammar’s relativeheader_tinto the absolutebody_off = pos + headerthe forward plane reads by. CRC is DEFERRED (the forward hop never walks a payload; the terminus / next hop verifies). One deliberate difference from the pre-grammar reader: the grammar rejects atype == 0x00or reserved-opt-bit header up front, so a malformed frame is dropped at this hop instead of forwarded — every caller already rejected such a header by its type check, so well-formed traffic is byte-identical.
-
struct fwd_pre_t¶
What a
dstpeek already learned about a FWD frame, so the head rebuild need not re-derive it (ADR-0038 inv. #1 — the forward hop parses each header ONCE).A forward hop used to walk the same TLV headers twice: the
dstpeek read the FWD header, the op VALUE, the dst PATH and every leading dst segment to decide where the frame goes — then rebuild_fwd_forward threw all of it away and re-read the identical bytes to build the outgoing heads. Profiling a 1-link hop put ~88% of it in header parsing, most of it that duplicate. Carrying the offsets forward is what removes it.Offsets, not spans, for the same reason the peeks are: the source may be a rope, so a caller re-slices from its own cursor. Filled by peek_fwd_dst_any, which is the ONLY parse of the frame’s leading headers a hop makes (#1794): the descent, the head rebuild, the bound arms, the terminus split and both refusal arms read these offsets and never re-read the headers behind them.
Two validities, kept apart on purpose. valid says the HEADERS were read — through the
dst, in a routable form or the empty one. strip_at says how much of thatdstthis hop consumes, which the peek knows only for a bounddstand the mount descent decides for a canonical one. The descent used to clear valid when it had no strip to hand over, which threw the parsed headers away with it and made the rebuild parse them again.Public Members
-
bool valid = false¶
The leading headers were read: a structured FWD, an op VALUE and a
dstin one of the forms fwd_dst_kind_t names other thanNONE. False ⇒ only the op fields below may be filled (see op_body_len).
-
std::size_t body_end = 0¶
End of the FWD body.
-
std::size_t op_pos = 0¶
Offset of the op VALUE TLV.
-
std::size_t op_total = 0¶
Its total size.
-
std::size_t op_body_off = 0¶
Its body — read to test for REPLY.
-
std::size_t op_body_len = 0¶
Its body length. Carried rather than re-checked so the rebuild keeps its own
body_len == 0rejection: the peek does NOT reject an empty op (such a frame falls through to the terminus decode today), and making the peek stricter would silently turn a dropped frame into a terminus one.The op fields are filled as soon as the op is read, before the
dstis judged, so the terminus split reads the opcode off them even for adstthe peek refuses. Non-zero here is exactly the framepeek_fwd_opanswers for.
-
std::size_t dst_body_off = 0¶
First byte of the dst PATH body.
-
std::size_t dst_end = 0¶
End of the dst PATH body.
-
std::size_t after_dst = 0¶
First byte after the dst PATH TLV.
-
std::size_t seg0_off = 0¶
The FIRST
dstsegment’s[body_off, body_len)— the gate’s own read, kept.peek_fwd_dst must parse this header anyway: “the leading child is a NAME” is the gate that decides a
dstis an address at all. It used to throw the parsed offsets away, and the descent immediately re-read the identical four bytes — one duplicatedparse_headeron EVERY forward hop, which is a per-frame cost the pre-lift peek did not pay (its one walk both gated and collected). Carrying the two integers forward removes the duplicate without moving the gate: the same header, read once, decides the same thing. Meaningless unless the peek answeredPATH.
-
std::size_t seg0_len = 0¶
Length of the first
dstsegment’s body.
-
std::size_t strip_at = 0¶
Where the surviving
dststarts after this hop consumes its leading records.The peek sets it past element 0 for a bound `dst` (each hop consumes exactly one element, RFC-0024 §4.1) and to @ref dst_body_off — nothing consumed — for a canonical one, whose strip the mount descent decides and writes here. The rebuild consumes `[dst_body_off, strip_at)` as given and refuses a value past @ref dst_end, so a strip the caller could not establish is a drop, never a re-walk.
-
wire::type_t dst_type = {}¶
The
dstheader’s type — and the type the rebuilt frame’s shrunkdstis headed with:PATH, orPATH_REFfor a BOUND address (RFC-0024 §4).The peek records the inbound type, so the outgoing one is the same by default: a bound
dststays bound across a forwarder hop (opt = 0— the body is a fixed-stride record array, not child TLVs), and a canonical one stays canonical. The ONE caller that changes it is the router’s session-delivery arm, the reverse-list delivery’s LAST hop (RFC-0024 §7.1 amendment 1): the consumed element was the final one and the egress is the accepted session, whose peer is an ORIGIN that never speaks the bound form, so it re-heads the emptieddstas a canonicalPATH— byte-for-byte the canonical delivery shape.Value-initialised to
type_t{}(no type), so a defaultfwd_pre_tstays all-zero and resetting one per frame is a plain clear; the peek writes it whenever it reads adst.
-
wire::opt_t fwd_opt = {}¶
The outer FWD header’s decoded
optbits — the peek’s own read, kept (#1109).The rebuild needs them for exactly one thing: preserving the frame’s trailer-timestamp across the hop (
opt.TS/opt.TFname the trailer window atbody_endthat the fresh head must re-claim and the gather must re-emit — without them the origin’s stamp is silently dropped at the first forwarder). Filled with the op fields.
-
bool valid = false¶
-
template<class Cursor>
class dst_seg_walk_t¶ A FORWARD-ONLY walker over a
dst’s leading NAME segments (#523).Hands out segment
ias[body_off, body_len)on demand. The mount descent walks adstTWICE by nature — once to fold the digest chain the registry scan filters on, once to CONFIRM the one candidate that survived it — so a purely forward walker re-parsed every header of the run per frame, and that showed up as a measured latency regression on theW <= 3shapes that already worked. The first kDstSegCacheSlots offsets are therefore remembered; past them a backwards ask resumes from the last cached one rather than from thedstbody, so even a very deep mount re-reads only the uncached tail.Its whole state is that fixed cache plus three integers — a CONSTANT, config-derived stack cost. That is what lets the mount width be unbounded: nothing here is sized by W, so the router’s stack frame does not grow with the deepest
dstit may ever see, and no deep-peek scratch has to be drawn from the injectedflatbackend either. (The measured alternative — a W-sized peek array — cost 592 B of rv32 stack at W=4 and 2912 B at W=33, per rope frame.)Offsets, not spans, for the reason every peek here uses them: the source may be a rope, so the caller re-slices from its own cursor.
It BORROWS the cursor, which must outlive it — and it is the only thing in this cluster that does. Every peek here (peek_fwd_dst, peek_fwd_first_dst_seg, rebuild_fwd_forward) consumes its cursor within the call and hands back offsets, so a caller may pass a temporary to any of them; this walker keeps reading through the cursor after the constructor returns, so a temporary there is a dangling read on the first at. Stated here because it was not, and because a caller cannot infer it from a signature that takes
const Cursor&like every other function on the page — the rvalue constructors below turn the mistake into a COMPILE error rather than a sanitizer finding.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
Public Functions
-
inline dst_seg_walk_t(const Cursor &cur, const fwd_pre_t &pre) noexcept¶
Walk the
dstwindowpredescribes, overcur.curis BORROWED — this object holds a pointer to it and reads through that pointer on every at, socurmust outlive the walk.preis copied (three integers), so it need not.
-
dst_seg_walk_t(Cursor&&, const fwd_pre_t&) = delete¶
A TEMPORARY cursor is refused at compile time (both value categories).
dst_seg_walk_t<span_cursor_t> w(span_cursor_t{frame}, pre);reads exactly like thepeek_fwd_dst(span_cursor_t{frame}, pre)one line above it and is the one spelling that is wrong: the temporary dies at the end of the full expression and every laterat()reads a dead stack slot. That is not hypothetical — it is what a test wrote and what ASan caught asstack-use-after-scopethroughread_packed_seg. Deleting these makes the shape unrepresentable instead of merely documented, at zero runtime cost; a caller with a temporary in hand names it first, which is what the three in-tree call sites already do.
-
dst_seg_walk_t(const Cursor&&, const fwd_pre_t&) = delete¶
The
constrvalue spelling of the same mistake — deleted for the same reason.
-
inline void prefill()¶
Segment
i's[body_off, body_len).Fill the inline cache NOW, in ONE tight loop.
The descent’s first act is to materialize the cached run, and doing it through
atmeant one out-of-line walk call per segment — a profile of the forward hop put a quarter of it there, because the header parse makes that half ofattoo large to inline. One call fills the whole run; every later ask is then the inlined cache half ofat.Purely an optimisation. Every answer is identical without it, and a
dstdeeper than the cached run is still walked on demand — which is exactly what makes this a CACHE and not the fixed peek window it replaced.- Return values:
std::nullopt – The
dsthas no segmenti— it ended, or the next child is not a NAME (a selector, say), which is where an ADDRESS stops.
-
inline std::optional<std::pair<std::size_t, std::size_t>> at(std::size_t i)¶
Segment
i's[body_off, body_len).- Return values:
std::nullopt – The
dsthas no segmenti— it ended, or the next child is not a NAME (a selector, say), which is where an ADDRESS stops.
-
inline std::optional<std::size_t> end_of(std::size_t i)¶
Where segment
iENDS — thestrip_atthe head rebuild wants.A packed record’s payload is its tail, so the consumed run ends at
body_off + body_lenof the last stripped segment. Returnsstd::nulloptif that segment does not exist. An escape record STEPPED OVER inside the run is inside[body, end_of(i))too, so the strip stays a single contiguous shrink.
-
struct control_head_t¶
A control frame (ADVERTISE / COMPACT / HANDLE_NACK) peeked off any cursor without a decoded tree (ADR-0055 §2).
Carries the
type, theu16label (child[0] VALUE, LE), and the[off, total)of child[1] — the route (ADVERTISE) / payload (COMPACT) sub-TLV, or{0, 0}for a bare-label HANDLE_NACK. Source-agnostic (offsets, not spans), so the caller re-slices from its own cursor (ADR-0053 ④b/⑥).
-
struct fwd_rebuild_t¶
The rebuilt forward-hop frame: fresh stack heads + the untouched source regions to interleave (ADR-0038 inv. #2 — ZERO heap on the forward hop).
Produced by rebuild_fwd_forward. Layout of the outgoing frame is
head1 · rem_dst · sel · head2 · src_body · tail, where every non-head region is an[off, len)window into the SOURCE cursor — the emit order is fixed by gather so the bytes a downstream child receives are byte-identical to the pre-extraction router.Public Functions
-
inline std::size_t ts_bytes() const¶
The preserved trailer timestamp’s WIDTH in bytes — 0 (none), 4 (narrow) or 8 (wide), as the producer spelled it (#1109).
The forwarder never chooses a width: it reports the one ts_window recorded from the inbound head, and rebuild_fwd_forward sets that word and the outgoing head’s TS/TF bits from the same
optin one place, so the emitted trailer and the header declaring it cannot disagree.
-
inline std::size_t ts_off() const¶
The window’s byte offset into the SOURCE cursor (bit 31 masked off).
-
inline bool ok() const¶
True ⇔ every head fits its stack buffer (else the caller drops).
-
template<class Cursor, class Push>
inline void gather(const Cursor &cur, Push &&push) const¶ Emit the outgoing frame’s regions, in wire order, through
push.Written ONCE over the cursor seam: each source region is emitted via
for_each_span, which yields exactly one sub-span for a contiguous source and one per straddled link for a rope — so only the caller’s iov container varies (a stack array for the span path, a pmr vector for the rope path). At most kFwdMaxIov regions for a contiguous source — see that constant for the region-by-region count. (This line previously said “at most 6”, which omittedhead2,mount_tlvand the peer pair.)- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope) — the SAME source rebuild_fwd_forward read the offsets from.
Push – Callable taking one
std::span<const std::byte>.
Public Members
-
stack_writer_t<kFwdHead1Cap> head1¶
FWD header + op (copied) + shrunk dst header.
-
stack_writer_t<kFwdSrcHdrCap> head2¶
The grown src PATH header.
-
std::span<const std::byte> mount_tlv¶
The inbound mount as ALREADY-ENCODED packed records, emitted as ONE span and never copied. Precomputed once per child (#508), so a hop does no per-segment work.
-
std::uint32_t ts_window = 0¶
The inbound frame’s trailer-TIMESTAMP window, re-emitted VERBATIM as the outgoing frame’s last bytes (#1109) — offset in bits 0-30, FORM in bit 31.
Zero ⇒ the frame carried no stamp, unambiguously: a trailer sits past a TLV’s own 4-byte header, so no stamped frame has a window at offset 0. Bit 31 (kTsNarrow) is the
opt.TFform the PRODUCER chose and this hop relays rather than picks — clear = the WIDE absolute stamp (8 bytes), set = the NARROW relative one (4). Read it through ts_off and ts_bytes, never raw. The CRC half of an inbound trailer is NOT here and never will be: the rebuilt body invalidates it, so it is dropped rather than forwarded stale (see stack_writer_t::header).One 4-byte word here, rather than an offset and a width at the end of this struct, is MEASURED, not tidiness (#1235). It occupies the alignment hole after extra_hdr, which keeps
sizeof(fwd_rebuild_t)at 256. The twostd::size_tfields this replaced pushed it to 272, and that alone — with the members NEVER READ, the ablation that proved it — costfwd-demux-fixed 79B/fan1/1epp50 +9.6% / throughput −9.3% on the pinned host. A 264-byte intermediate (twostd::uint32_ts) still cost +9.5%, so the step is at 256 and it is the whole object’s size that matters, not the field count. Packing the form INTO the word rather than deriving it from the emitted head is measured too: the derived form left the rope hop’sroute_fwd_forward16 B larger and the demux row 5 ns short of the parent, where this shape returns both to it.A frame whose body ends past kTsNarrow cannot express its window here, so the rebuild drops the stamp AND its header bits together rather than declaring a trailer it will not emit — a 2 GiB single TLV, which no shipped transport will carry.
-
std::string_view extra_seg¶
One dynamically-named trailing mount segment — a bus PEER, whose name is not known until the frame arrives and so cannot be precomputed. Empty means none; referenced, not copied, so it must outlive gather.
-
std::size_t rem_dst_off = 0¶
Remaining dst body after the stripped segment.
-
std::size_t rem_dst_len = 0¶
Length of the remaining dst body.
-
std::size_t sel_pos = 0¶
The optional FIELD selector TLV; 0 len ⇒ none.
-
std::size_t sel_total = 0¶
Total bytes of the selector TLV.
-
std::size_t src_body_off = 0¶
The original src PATH body.
-
std::size_t src_body_len = 0¶
Length of the original src body.
-
std::size_t tail_off = 0¶
Bytes after src (payload etc.).
-
std::size_t tail_len = 0¶
Length of the tail region.
-
stack_writer_t<4 + wire::kPathRefElementBytes> mint¶
This hop’s contribution to a mint answer: a fresh
PATH_REFheader plus ONE 8-byte element (RFC-0024 §7.1 step 2). Empty ⇒ this frame carries no mint.Written only on a forwarded REPLY whose last child is already a
PATH_REF. The element goes FIRST in the new body, ahead of ref_body_off — the elements the hops further out have already contributed — because the list is origin-first and this hop is nearer the origin than every host that has touched the reply so far. That is the mirror of the waysrcaccumulates on the way in (RFC-0004 §B), and it is a rope operation on the egress rather than a rewrite: the existing elements are referenced, never copied.
-
std::size_t ref_body_off = 0¶
The trailing
PATH_REF’s existing element array.
-
inline std::size_t ts_bytes() const¶
-
template<std::size_t N>
class stack_writer_t¶ A fixed-capacity stack byte-writer — the zero-heap head builder for the forward hop (ADR-0038 inv. #2).
The zero-heap counterpart of the old vector-based header builder: “the fresh
header bytes … a stack std::array, not a std::vector”. Bounded by the wire header widths + one NAME (kMaxSegmentBytes), so
Nis a small compile-time constant; a write past capacity clamps to empty (the caller treats an empty head as a drop — never a buffer overrun).- Template Parameters:
N – The writer’s stack capacity in bytes.
Public Functions
-
inline void header(wire::type_t type, std::size_t body_len, wire::opt_t trailer = {})¶
Append a structured TLV header (
plset,llauto-widened) forbody_len.trailercontributes its TS/TF bits alone (#1109) — the builder can now EXPRESS a trailer, in either form, so a forwarded frame’s origin stamp survives the head rebuild (the caller that sets them owns emitting the trailer bytes after the body). CR never crosses: a rebuilt body invalidates any inbound CRC by construction, so preserving the bit would mint a frame its own receiver rejects ascrc_fail.
-
inline void header_route(wire::type_t type, std::size_t body_len)¶
Append a ROUTE header — a
PATH,PATH_REForPATH_REF_REVERSE— forbody_len, withopt.PL = 0.Its own method rather than header, because a route body is NOT a child run: a packed
PATHbody is a record run (RFC-0018 §5) and aPATH_REFbody a fixed-stride element array (RFC-0024 §4.2), andopt.PL = 1on either would make a generic walker read the first body bytes as a TLV header and mis-frame the whole address — a MUST, not a preference.LLwidens for aPATH, whose body may legally pass 0xFFFF. It never widens for the bound types: their element bound caps the body at 2040 bytes, so abody_lenthat claims otherwise overflows rather than widening. One method for every route type is what lets the forward hop head its shrunkdstwith the type the peek carried (fwd_pre_t::dst_type) instead of choosing between two writers.
-
inline void path_seg(std::string_view s)¶
Append one packed PATH segment record over
s([u8 len][bytes], RFC-0018). An emptyswould spell the §5.4 escape, so it overflows rather than mints.
-
inline void raw(std::span<const std::byte> bytes)¶
Copy opaque
bytesverbatim (the op TLV).
-
inline std::span<const std::byte> span() const¶
The written bytes.
- Return values:
empty – A write overflowed
N— the caller must drop the frame.
-
inline bool ok() const noexcept¶
False ⇔ a write overflowed
N.
Public Static Functions
-
static inline constexpr std::size_t header_bytes(std::size_t body_len) noexcept¶
The wire bytes a header this writer emits for
body_lenoccupies — 4, or 6 once the length needs a u32.The writer reports its own width, so a caller sizing a parent body counts a child header by the rule that will write it rather than by a copy of that rule.
-
template<class Cursor>
std::optional<fwd_hdr_t> tr::net::read_fwd_header(const Cursor &cur, std::size_t pos, wire::grammar::crc_check_t crc = wire::grammar::crc_check_t::DEFER)¶ Read ONE TLV header at absolute offset
posofcur(no descent).Templated over the grammar
Cursorconcept (ADR-0053 ④b): the forward plane reads its dispatch offsets through the SAME byte-source seam the one grammar validates through —span_cursor_tfor the contiguous path, the rope cursor for a scatter-gather frame, with no per-cursor offset math.cur.region(pos, …)narrows either source in O(1) before the header parse.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
pos – Absolute offset of the header to read.
- Return values:
std::nullopt –
posis out of range or the grammar rejects the header.
-
enum class tr::net::fwd_dst_kind_t : std::uint8_t¶
Which of the two routable
dstforms a FWD frame carries.The two forms are mutually exclusive by the
dst’s own type code, and telling them apart is ONE read of the frame’s three leading headers — so it is one function that answers, not two gates run in sequence. Running them in sequence is what put a whole second header walk on every bound frame (a shipped shape once RFC-0024 lands) while buying the canonical form nothing at all.Values:
-
enumerator NONE¶
Not a structured FWD, or a
dstin neither routable form.
-
enumerator PATH¶
A canonical
PATHof NAMEs — the mount descent’s address.
-
enumerator PATH_REF¶
A BOUND address (RFC-0024 §4) — a fixed-stride element array.
-
enumerator PATH_LABEL¶
A canonical
PATHwhose FIRST record is an escape — RFC-0027’s labelled address.Told apart from the plain
PATHanswer because the two take different routes and must not be confused: a canonicalPATHopens with a NAME the mount descent folds a digest over, and this opens with a record that is not a name at all. Descending it would read a peer-supplied slot index as UTF-8.A node that does not implement minting treats it exactly as it treated a non-NAME leading child before RFC-0027 existed — it names no mount here, so the frame falls to the terminus arm and is refused there. That is why this is a distinct answer rather than a flag on the plain
PATHanswer: the ONE branch a non-minting node takes on it is the one it already took, and it takes it without a table, without a lookup, and without reading the record.
-
enumerator EMPTY¶
A canonical
PATHwith NO records — a fully consumed address.The shape a terminating REPLY carries (its
dstis the request’s accumulatedsrc, and every hop on the way back consumed its own part of it), and the shape of a request addressed to this node’s own root. There is nothing to descend, so it takes neither the mount descent nor a bound arm; it terminates here. It is an answer of its own, with the headers kept in the filledfwd_pre_t, so the terminus split and the refusal correlation read their offsets instead of parsing the same headers again (#1794).
-
enumerator NONE¶
-
template<class Cursor>
fwd_dst_kind_t tr::net::peek_fwd_dst_any(const Cursor &cur, fwd_pre_t &pre, std::size_t &ref_count)¶ Open the
dstwindow of a FWD frame and say WHICH form it is — the routing gate.Fills
prewith everything the descent, the bound hop and the head rebuild need about the frame’s structure: where the op VALUE and thedstbody are, and where the body ends. It reads NO segments and materializes nothing, so its cost and its stack are the same whatever thedst’s depth or element count.The arms diverge only at the
dstheader’s type code (plus theEMPTYanswer, a canonicalPATHwith no records):PATH— the canonical address, a packed record run withopt.PL = 0(RFC-0018). The leading record must be a LITERAL segment (adstwhose first record is the label escape is not an address this node can descend), and fwd_pre_t::strip_at starts at the body because onlystrip_kcan say how much of it this hop consumes.PATH_REF— the bound address. The four STRUCTURAL rules (opt.PL = 0,opt.LL = 0,length % 8 == 0,length <= 2040) are checked throughtr::wire::path_ref_body_valid— the one locus that owns them — and fwd_pre_t::strip_at is known HERE, past element 0: each hop consumes exactly one element (§4.1), with no descent to wait for.
Note
flatten— see rebuild_fwd_forward for the measurement. The four read_fwd_header calls below are this function’s whole body, and each returns a ~56-bytestd::optional<fwd_hdr_t>that an out-of-line call must return through memory.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
pre – Filled on every non-
NONEanswer, withvalidset. OnNONEit is reset withvalid = false, and only its op fields may be filled (when the frame is a structured FWD with an op VALUE), which is what the terminus split reads.ref_count – Written with the
PATH_REFelement count on thePATH_REFanswer, 0 otherwise. 1 is the terminus (the residual is this node’s own reference to the target vertex); **> 1 is a forwarder hop**; 0 is a route with no hops, which the codec deliberately admits and the router refuses.
-
template<class Cursor>
bool tr::net::peek_fwd_dst(const Cursor &cur, fwd_pre_t &pre)¶ Open the
dstwindow of a FWD frame — the mount descent’s gate, read by OFFSET.The canonical arm of peek_fwd_dst_any, for a caller that routes only the canonical form (the unit tests and
bench_forward_demux). Fillsprewith everything the descent and the head rebuild need about the frame’s structure: where the op VALUE and thedstPATH body are, and where the body ends. It reads NO segments and materializes nothing, so its cost and its stack are the same whatever thedst’s depth — the point of #523. Segments are then walked lazily through dst_seg_walk_t, one at a time, only as far as the registry actually asks.This replaces
peek_fwd_dst_segs, which eagerly filled akMountPeekMax-sized array of offsets. That array was the width bound’s last physical residue: it decided in advance how many segments the descent could ever look at, and sizing it by the widest mount would have put a W-sized array on every rope frame (measured on rv32: 592 B of stack at W=4, 2912 B at W=33). Nothing here is sized by a width at all.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
pre – Filled on success; reset with
valid = falseon every failure, so a caller cannot pass stale offsets to the rebuild.
- Return values:
false – Not a structured FWD with an op VALUE, a non-empty
dstPATH, and a leading NAME segment — the caller falls through to the terminus/control arms exactly as it did on the oldn == 0. A well-formed BOUNDdstis among the false answers, andpreis cleared for it too.
-
template<class Cursor>
std::optional<std::size_t> tr::net::peek_fwd_dst_ref(const Cursor &cur, fwd_pre_t &pre)¶ Open the
dstwindow of a BOUND FWD frame — the bound hop’s gate (RFC-0024 §5).The bound arm of peek_fwd_dst_any, for a caller that has only the bound question to ask (the conformance and unit tests). The router asks BOTH questions at once, because a frame is one form or the other and finding out twice is a second header walk for nothing.
Fills
preas peek_fwd_dst_any does on itsPATH_REFanswer, and sets fwd_pre_t::strip_at past element 0 — the ONE element this hop consumes (§4.1: each hop consumes element 0 and forwards the remainder, the same monotone shrink the canonicaldstperforms, which is why a bound path is loop-free by construction and needs no visited set).The four STRUCTURAL rules (
opt.PL = 0,opt.LL = 0,length % 8 == 0,length <= 2040) are checked throughtr::wire::path_ref_body_valid— the one locus that owns them — so a frame that fails any of them is not a bound address and falls through to the caller’s terminus arm, where the resolver refuses it as it refuses every other malformeddst.- Return values:
std::nullopt – Not a structured FWD whose
dstis a structurally validPATH_REF.- Returns:
The element count on the wire. 1 is the terminus (the residual is this node’s own reference to the target vertex); **> 1 is a forwarder hop**; 0 is a route with no hops, which the codec deliberately admits and the router refuses.
-
template<class Cursor>
wire::path_ref_element_t tr::net::read_path_ref_element(const Cursor &cur, std::size_t off)¶ Read the 8-byte
PATH_REFelement atoffthrough the cursor seam.Byte-wise rather than through
tr::wire::path_ref_element_at, because on the rope tier an element may straddle a link boundary and there is then no span to hand that function. Eightbyte_atcalls need no scratch, no stitch slot and no flatten, which is the property that lets a bound hop stay allocation-free on a fragmented frame; the codec’s own reader stays the one that serves a contiguous body.Note
Precondition:
off + 8is inside the frame — the caller has already had the body shape settled by peek_fwd_dst_ref and knows the element count.
-
struct trailing_mint_t¶
The trailing bound-path child a forwarded frame carries — the mint list so far, in whichever direction the caller asked for.
See also
Public Members
-
std::size_t pos = 0¶
Offset of the
PATH_REFchild’s own header.
-
std::size_t body_len = 0¶
Length of the element array already on the wire.
-
bool can_contribute = false¶
False ⇔ the list is at the normative element cap and one more would not be spellable, so this hop MUST strip it rather than relay it (§7.1 erratum 1).
-
std::size_t pos = 0¶
-
struct no_mint_t¶
The mint supplier of a hop that contributes nothing — rebuild_fwd_forward’s default, and the shape every caller outside the router has.
A callable rather than a pointer so the router’s own supplier can be a closure that runs ONLY when the frame turns out to carry an extendable mint answer. Returning
nullopthere does not relay the answer: the rebuild STRIPS it, which is the §7.1 erratum-1 rule.Public Functions
-
inline std::optional<wire::path_ref_element_t> operator()() const noexcept¶
Nothing to give.
-
inline std::optional<wire::path_ref_element_t> operator()() const noexcept¶
-
template<class Cursor>
std::optional<trailing_mint_t> tr::net::peek_trailing_mint(const Cursor &cur, std::size_t from, std::size_t end, wire::type_t want = wire::type_t::PATH_REF)¶ Where a forwarded frame’s trailing mint list sits, if it carries one (RFC-0024 §7.1).
A mint list rides its frame as the LAST child, so a hop that wants to contribute its own element looks exactly there and nowhere else. The presence of that child IS the signal that the origin asked for a mint — a hop holds no per-flow state and has nothing else to read it from, which is what keeps the accumulation stateless.
The two directions are told apart by TYPE, never by position (RFC-0024 §7.1 amendment 2): the forward mint ANSWER on a reply is a
PATH_REF(0x14), the REVERSE list on a mint-flagged request aPATH_REF_REVERSE(0x15).wantis that discriminant, and it is free: the loop below already compares each tail child’s type byte, so asking for the other constant is the same compare. A positional rule (“the only trailing child”) would have cost the same here and foreclosed a rawPATH_REFpayload on a mint-flagged WRITE, which is why the type carries the role.**
wantis a RUNTIME parameter, and that is measured, not stylistic.** As a template parameter it reads better and folds to an immediate — and it costs +14% on the fixed forward hop and +23% on the 64-link demux scan (bench_forward_demux, reproduced againstmain), because two instantiations stop being one shared out-of-line function and get inlined into the twonoinlinemint helpers instead, which repartitions theflattenedrebuild_fwd_forwardthe whole demux path runs. This is exactly the hazard the mint helpers’ ownnoinlinenotes describe. One shared copy, one register argument: the pre-amendment code shape, and level with it.FINDING the answer and being able to ADD to it are two different answers, and the caller needs both: a hop that finds a list it cannot extend MUST STRIP it (§7.1 erratum 1), never relay it. A relayed list that skips a hop is not a shorter route, it is a WRONG one — see the strip branch in rebuild_fwd_forward for the mis-route it produces. Reporting a full-cap list as “not found” would take exactly that forbidden branch, so the cap rides back as trailing_mint_t::can_contribute rather than as a
nullopt.- Parameters:
cur – Cursor over the frame.
from – First byte after
src— where the frame’s trailing children begin.end – End of the FWD body.
want – The mint list’s type:
PATH_REFon a reply,PATH_REF_REVERSEon a mint-flagged request (RFC-0024 §7.1 amendment 2). A RUNTIME parameter, and deliberately so — see the note above on why a template one is not free here.
- Return values:
std::nullopt – No trailing child of type
wantat all, or a malformed tail. The frame carries no mint exchange in this direction and is forwarded untouched.
-
template<class Cursor, class MintFn>
void tr::net::rebuild_reply_mint(const Cursor &cur, std::size_t pos, std::size_t body_end, MintFn &mint_fn, fwd_rebuild_t &r)¶ The forward hop’s stack object stays at or under 256 bytes — a MEASURED ratchet (#1235), not a style rule.
This object is built on the stack of every forwarded frame, and its size is load-bearing on the shipped fast path: growing it to 272 by adding two
std::size_tfields costfwd-demux-fixed 79B/fan1/1epp50 +9.6% / throughput −9.3% on the pinned host, and an ablation that added the same 16 bytes and NEVER READ THEM cost exactly the same — so the price is the size, not the work. A 264-byte intermediate was still +9.5%. The regression shipped for 16 samples because it walked under every PR-gate threshold; this assert is what makes the next such growth a compile error instead. A field that will not fit belongs in an alignment hole, or packed into a word that is already there (see fwd_rebuild_t::ts_window).The mint accumulation half of a forwarded REPLY’s rebuild (RFC-0024 §7.1 step 2), deliberately kept OUT OF LINE.
noinlineagainst rebuild_fwd_forward’sflatten, and it is a measurement, not a preference.flattenpulls everything a function calls into it, so inlined this dragged peek_trailing_mint’s header loop into the rebuild’s body — on the branch a REQUEST hop never takes. The extra front end that bought costbench_forward_rope+13% at fan 2 in branch mispredicts (3x armA’s count underperf stat, on ~equal instructions): a shipped shape paying for a form its frames cannot be. Out of line, the request hop sees one not-taken branch and the rope arm measures at or belowmainat every fan.Writes
r'stail and mint fields. A contribution leaves fwd_rebuild_t::mint written (the re-headed child and this hop’s element); contributing nothing — which INCLUDES the strip cases (no element to give, or a list already at the cap) — leaves it empty, and the caller sizes the body from those two fields alone. A reply with no mint answer at all leavesruntouched.
-
template<class Cursor, class MintFn>
void tr::net::rebuild_request_reverse_mint(const Cursor &cur, std::size_t pos, std::size_t body_end, MintFn &mint_fn, fwd_rebuild_t &r)¶ The REVERSE-direction mint on a forwarded mint-flagged REQUEST (RFC-0024 §7.1 amendment 1) — the request-side mirror of rebuild_reply_mint, equally OUT OF LINE and for the same measured reason (its
noinlinenote applies verbatim: the tail walk must not be flattened into the hop every unflagged frame runs).Three outcomes, all normative:
Extend. The request’s last child is already a
PATH_REF_REVERSE(0x15, the reverse list’s OWN type since RFC-0024 §7.1 amendment 2 — never a position) andmint_fnyields this hop’s element for the identity the frame ARRIVED on — its connection vertex for a point-to-point link, or the accepted session’s identity vertex for a bus session. The element is PREPENDED (the list runs responder-first), exactly the fwd_rebuild_t::mint machinery the reply side uses.Create. No reverse child yet — this is the FIRST forwarding hop (the origin never emits the child) — and
mint_fnyields an element: a fresh one-elementPATH_REF_REVERSEis appended as the new last child. “A hop with no reverse child yet
MAY create it”; the reference core participates.
Strip. A reverse child exists but this hop cannot contribute (no identity vertex, a saturated generation, a full list): the WHOLE child is removed. Erratum 1’s rule direction-reversed and equally forced — a list that skips a hop is a wrong route, the §5.3 mis-route class, so it is all-or-nothing over the reverse list alone.
The unflagged request never reaches here (the caller gates on op bit 7), so the ordinary forward hop pays one not-taken branch, exactly as it does for the reply mint.
Writes
r'stail and mint fields, as rebuild_reply_mint does: an extension or a creation leaves fwd_rebuild_t::mint written (a creation with fwd_rebuild_t::ref_body_len = 0), and strip or no-op leaves it empty.
-
template<class Cursor>
std::optional<std::pair<std::size_t, std::size_t>> tr::net::peek_fwd_first_dst_seg(const Cursor &cur)¶ The forward dispatch decision, read by OFFSET with no allocation (ADR-0038 inv. #1, ADR-0039).
A FWD whose first
dstsegment names a transport child is a forward hop that never needs the decoded tree. Returns the[body_off, body_len)of the first packed dst-segment record iff the frame is a structured FWD with an op VALUE + a non-empty dst PATH; nullopt otherwise (malformed, non-FWD, or empty dst ⇒ the caller falls back to the full-decode terminus path). Offsets, not a span, so the result is source-agnostic — the caller re-slices the segment bytes from its own cursor (contiguous or rope).- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
-
template<class Cursor>
std::optional<graph::fwd_op_t> tr::net::peek_fwd_op(const Cursor &cur)¶ Read the FWD op discriminant (child[0], a VALUE u8) by OFFSET.
The terminus split (REPLY → originator sink vs request → arena resolve) without a decode.
- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
- Return values:
std::nullopt – Not a structured FWD, or its op VALUE is missing/empty.
-
template<class Cursor>
std::optional<control_head_t> tr::net::peek_control(const Cursor &cur, wire::grammar::crc_check_t crc = wire::grammar::crc_check_t::DEFER)¶ Peek a control frame’s head (type + label + child[1] window) by OFFSET.
- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the frame’s first byte.
- Return values:
std::nullopt – Malformed, or not a structured ADVERTISE / COMPACT / HANDLE_NACK leading with a ≥2-byte VALUE label.
-
template<class Cursor, class MintFn = no_mint_t, class ReverseMintFn = no_mint_t>
std::optional<fwd_rebuild_t> tr::net::rebuild_fwd_forward(const Cursor &cur, const fwd_pre_t &pre, std::span<const std::byte> mount_tlv, std::string_view extra_seg, MintFn mint_fn = MintFn{}, ReverseMintFn reverse_mint_fn = ReverseMintFn{}, std::span<const std::byte> reply_label = {})¶ The forward hop’s head rebuild, read entirely by OFFSET — no decoded tree (ADR-0038 inv. #1).
Layout:
FWD{ op VALUE, dst PATH, FIELD? sel, src PATH, tail }— consumes the leadingdstrecords the routing decision named (fwd_pre_t::strip_at), grows src bymount_tlv(unless the op is REPLY: a reply accumulates no return route, RFC-0004 §B), and synthesizes the two fresh stack heads. The caller scatter-gathers the result via fwd_rebuild_t::gather — no payload copy, zero heap.One parse per hop (#1794). Every header in front of the selector was read by peek_fwd_dst_any, and this function reads none of them again: it starts at fwd_pre_t::after_dst and parses only the selector and
srcheaders the peek never reached. It used to carry a second, self-parsing arm for a caller with no peek in hand, and a segment walk for a caller that had not recorded where its strip ended; both are gone, so the header logic of a forward hop lives in one function and a fix to it lands everywhere.strip-K and the symmetric return route (ADR-0061 + its erratum). A mount is addressed by its full path
/net/<module>/<name>[/<peer>], so a hop consumes K segments rather than one, andsrcgrows by that SAME run — not by a single NAME. Growing by a bare name would make the return route ambiguous the moment connection names are per-module-scoped (/net/ws-client/foovs/net/tcp-client/foo), because a reply’sdstIS the accumulatedsrc. Prepending the whole mount keeps routing-address==vertex-path in BOTH directions, so a reply resolves through the identical descent as a forward.This is the one region a REPLY’s
srcmay grow by, and it exists because 6.1 requires the first reply to reach the original sender already minted: a hop’s local part is stripped from the reply’sdstand echoed nowhere else, so a label that replaced it would otherwise have no way home. What is prepended is strictly SHORTER than the string run it stands for (7 bytes against a mount run’s 10 and up), which is 6.1’s “replaces, never appends” in the accounting 6.1 itself uses — the comparison is against the string spelling of the same accumulation, not against a reply that accumulates nothing.It narrows RFC-0004 B’s “a reply accumulates no return route” to NON-MINTING hops, which is a normative tension RFC-0027 6.1 already decided at acceptance and which is recorded as an erratum in that RFC’s log rather than assumed here. Empty is the default and the conformant behaviour, so a node that never mints is byte-unchanged.
Note
A returned rebuild may still have
!ok()(an oversized op TLV overflowed a head) — the caller must check and drop, never overrun.Note
**
flatten, and it is measured.** read_fwd_header returnsstd::optional<fwd_hdr_t>— six words — so an OUT-OF-LINE call returns it through memory and the caller re-loads every field. Inlined it is registers. Which way the compiler goes is a budget decision it makes per caller, and it flipped the wrong way for the three header readers on the forward hop once the descent’s cold arm was moved out of line andon_frame_implshrank: aperfprofile of aW = 3hop put 58% of it inside an out-of-lineread_fwd_header, against 4% on the pre-lift build where the same calls were inlined.flattenon the three functions that read headers in a loop (here, peek_fwd_dst, and the router’sresolve_mount_at) is worth 6-8 ns per hop across everyWmeasured. It is deliberately NOTalways_inlineon read_fwd_header itself: that inlines it into the cold control-frame and terminus paths too and measured +16 to +24% — strictly worse than doing nothing.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
- Parameters:
cur – The cursor positioned at the inbound FWD frame’s first byte.
pre – What peek_fwd_dst_any read off this frame, with fwd_pre_t::strip_at set to the end of the records this hop consumes (the mount descent’s answer, or element 0 for a bound
dst) and fwd_pre_t::dst_type to the type the shrunkdstis headed with.mount_tlv – This node’s mount path for the link the frame arrived on, ALREADY ENCODED as a run of packed records (precomputed per child, #508).
extra_seg – One further mount segment whose name is only known now — a bus PEER. Empty when the mount is fully precomputed.
mint_fn – This hop’s mint contribution, supplied LAZILY: invoked at most once, and only on a forwarded REPLY that actually carries an extendable mint answer. Laziness is the whole point — deciding eagerly meant reading the op byte a second time on EVERY forwarded frame, including the request hops that can never mint, and that duplicate read is a rope-cursor byte walk on a fragmented frame. Defaults to no_mint_t, the hop that contributes nothing.
reverse_mint_fn – The REVERSE-direction twin, invoked at most once and only on a mint-flagged request (RFC-0024 §7.1 amendment 1).
reply_label – RFC-0027 6.1’s minted spelling of THIS hop’s own local part, as the already-encoded 7-byte label element, or empty to mint nothing.
- Return values:
std::nullopt – The frame is not a well-formed forwardable FWD (no peeked headers, an empty op, a strip past the
dst, an unspellable peer segment, or asrcthat cannot be relayed) — the caller drops it.
-
constexpr std::size_t tr::net::kDstSegCacheSlots = graph::kCacheLineBytes / sizeof(std::pair<std::size_t, std::size_t>) < 2 ? std::size_t{2} : graph::kCacheLineBytes / sizeof(std::pair<std::size_t, std::size_t>)¶
A FORWARD-ONLY walker over a
dst’s leading NAME segments (#523).Hands out segment
ias[body_off, body_len)on demand and remembers where it stopped, so the descent’s natural ascending access pattern (0, 1, 2, …) costs ONE walk of the headers however many slots ask. A request for an index BEHIND the cursor restarts from thedstbody — which happens only when a narrower registry slot is tested after a wider one, and costs a handful of 4-byte header reads.Its whole state is three integers. That is what lets the mount width be unbounded: there is no array to size, so the router’s stack frame does not grow with the deepest
dstit may ever see, and no deep-peek scratch has to be drawn from the injectedflatbackend either.Offsets, not spans, for the reason every peek here uses them: the source may be a rope, so the caller re-slices from its own cursor.
How many segment offsets a dst_seg_walk_t keeps inline: ONE CACHE LINE’s worth.
NOT a width bound and not a new constant to raise — it is a CACHE. The walk is correct, and gives the same answers, at any width with any value here (including the structural floor of two); past the cached run it simply re-reads headers. Nothing about which mounts resolve depends on it, which is exactly what
kMountPeekMaxcould not say.Sized from
tr::kCacheLineBytes, the config quantity a target already declares (ADR-0068 §3;-DLIBTRACER_CACHE_LINE_BYTES), because the whole point is that the cached run costs no extra line fetch. A config that declares no cache line (0, the single-core profile) still gets the floor of TWO — the two the descent structurally needs, since it reads segmentkto see whether the address continues and then asks where segmentk-1ended.- Template Parameters:
Cursor – A grammar byte-source cursor (span or rope).
-
constexpr std::size_t tr::net::kFwdHead1Cap = 64¶
Capacity of the forward hop’s first head: FWD hdr(≤6) + op TLV(small) + PATH hdr(≤6).
-
constexpr std::size_t tr::net::kFwdSrcHdrCap = 6¶
Capacity of the forward hop’s second head: the grown src PATH header alone.
-
constexpr std::size_t tr::net::kFwdMaxIov = 10¶
Upper bound on the regions fwd_rebuild_t::gather emits for a CONTIGUOUS source.
Structural, and now counted from fwd_rebuild_t::gather’s actual emit sequence rather than budgeted — one region per
push, in wire order:head15.mount_tlv(ONE span, whatever the mount’s width)rem_dst6.extra_hdr_ at most one PAIR, for a dynamicallysel7.extra_seg/ named bus peerhead28.src_bodytailtrailer TS (the preserved stamp window, #1109)
A rope source may split any region further and so gathers into a growable container instead; this bound is the CONTIGUOUS arm’s, and only that arm uses a stack array.
It previously read
6 + 2 * kMountPeekMax= 14, describing a header-and-bytes pair per prepended mount segment. That emission has not happened since #508, which made the mount run one precomputed span — so the constant was over-provisioned by 5 and, worse, was the wrong SHAPE: tied to mount width when the region count has been independent of it for some time.The shape mattered. Had the 2026-07-30 mount-depth ruling been implemented by re-deriving this as
6 + 2 * depth, it would have crossed 17 at depth 6 — and 17 is exactly where both shipping transports fall back to a heap-allocated iovec table (transport_udp.cpp,transport_tcp.cpp,kMaxInlineIov = 16; measured bybench_transport_iov). That would have put a per-frame allocation on every deep-mount forward hop whilebench_forward_heapstill reportedallocs=0, because that gate drives a stub link which never assembles an iovec.At 10 the headroom to the transport spill is 7 regions. Keep the mount one span and this constant does not move when the descent is uncapped.
The bound-path mint accumulation (RFC-0024 §7.1) does not move it either, and this is counted rather than assumed. A forwarded REPLY’s mint adds two regions — this hop’s 12-byte head-plus-element, and the elements already on the wire — but a REPLY grows no
src, so the mount run and the bus peer’s header-and-segment pair (regions 5-7) are empty on exactly the frames that use them. The two sets are mutually exclusive byis_reply: a REQUEST emits at most head1, remaining dst, selector, head2, mount, peer header, peer segment,srcbody, tail and trailer TS = 10; a REPLY at most head1, remaining dst, selector, head2,srcbody, tail, mint head, mint elements and trailer TS = 9. It was briefly raised to 11 by adding the request and mint sets together, which is a bound no frame can reach — and the constant is measured, not defensive: that change moved code placement enough to costbench_forward_ropea disjoint +13% at fan 2 in branch mispredicts, on a shape that emits none of the regions it was raised for. (The +1 here, by contrast, is a region a stamped frame really emits — the #1109 trailer-TS window, region 10 above.)
The /net connection surface¶
-
class transport_vertex_t¶
Groups connection vertices under
/netand makes each a/vertex (ADR-0027).Construct over a live graph::graph_t and
fwd_router_t.There is exactly ONE wire creation door: the per-module CREATOR ENDPOINT (RFC-0014 §2, S2b). register_module mints
<net_root>/<module>/conn(thekConnEndpointNameconstant), and aSPEC{ name, config }written there creates<net_root>/<module>/<name>and wires its transport into the router’s child_registry_t — the single NAME→link demux table — while aNAME{ <name> }removes it. The module in the path fixes both the transport and the role, so the SPEC carries neither atypenor arole.The superseded global
write /net:children[] += SPEC{type = "client"|"listener", …}spelling is retired (RFC-0014 S7, executing the supersession ADR-0059 ruled): theclient/listenercatalog types are no longer registered, so that write now answersSCHEMA_NOT_FOUNDlike any other unregistered type./net:children[]remains a read-only ENUMERATION of this plane’s modules.The MODULE is therefore known POSITIONALLY, from the endpoint’s own path, before any transport is resolved — which is what the mount, the routing key and the staging key all need, since each is
<module>/<name>and a NAME alone names none of them (#883). A configkind, when present, must be one the module declares (register_module); a kind-less SPEC — the provide_link spelling — takes the module’s own declared kind, and a module declared for two kinds is refusedTYPE_MISMATCHrather than resolved by declaration order.The transport then comes from one of two sources, in precedence order within that module:
a link staged via provide_link under exactly this
<module>/<name>(borrowed; the caller owns it) — the test/manual seam for loopback channels and transports the catalog doesn’t cover;otherwise, the transport-factory catalog: the config’s
kindselects a factory (built-inudp/tcp/ws, or any registered via register_transport_type), which CONSTRUCTS the real socket from the parsed conn_settings_t; the connection vertex OWNS it, and its link state is written up on successful construction.
A staging under some OTHER module that happens to share the leaf NAME is a different connection: it is neither used nor consumed here.
Destruction semantics (honest): there is no child-removal / connection-teardown model yet (#66), so an owned transport lives as long as this
transport_vertex_t— its recv thread is joined when this object destructs. Declare thetransport_vertex_tAFTER the graph and router it binds (the usual stack order), so owned transports stop delivering frames before the router they feed is gone.Public Types
-
using transport_factory_t = net::transport_factory_t¶
The factory signature a transport kind registers with — the namespace-scope tr::net::transport_factory_t, kept here so existing spellings still compile.
Public Functions
-
transport_vertex_t(graph::graph_t &graph, fwd_router_t &router, std::string_view net_root = "/net", mem::mem_backend_t *rx_backend = &mem::net_backend(), mem::block_source_t *egress_src = &mem::net_source())¶
Bind to
graphandrouterand register theclient/listenercatalog types under the/netparent (which is registered if absent).Also registers the built-in transport factories:
udp(DIAL: bind an ephemeral port, peer =addr:port; LISTEN: bindport, peer learned from inbound datagrams) andws(DIAL:ws_client_transport_t(addr, port)— a synchronous connect + RFC 6455 handshake, run by the liveness engine on its first DIAL rather than at creation; LISTEN:ws_server_transport_t(port)— accepts MANY concurrent inbound peers (#362), with the ws-privatepeer_named/max_peersconfig keys selecting the ADR-0044 bus facet and the admission cap).All three built-ins are registered with
kBuiltinPointToPointTraits(#1548), so their DIAL connections are ENGINE-MANAGED: creation constructs no socket, mints the vertexDORMANT, and dials on first use. Their LISTEN halves bind eagerly as before.- Parameters:
net_root – The parent path for connection vertices (default “/net”).
rx_backend – The RX memory seam config-constructed view-delivering transports draw their inbound frame segments from (ADR-0042 §2): the built-in
udpfactory passes it to every socket it constructs, so a creator-endpoint-created connection participates in owning delivery. Default: the process net sub-pool; a bounded host injects its pool over its static slab. Must outlive this object (and thus every owned transport).egress_src – The EGRESS twin of
rx_backend(#873 family 1, ADR-0079’s net-plane failable store): theblock_source_tevery socket these built-in factories construct draws its per-send gather block from — the basetransport_t::send(iov)temporary and theiov_table_toverflow, both sized by the SENDING peer. Sizing it is what bounds this node’s egress allocation; exhaustion drops the frame and counts it, exactly as it already does. Default: the process net sub-pool (#1777). Must outlive this object (and thus every owned transport). A kind’s own factory registered later via register_transport_type reaches the same store through egress_source.
-
transport_vertex_t(graph::graph_t &graph, fwd_router_t &router, std::string_view net_root, mem::mem_backend_t *rx_backend, slim_net_t, mem::block_source_t *egress_src = &mem::net_source())¶
SLIM ctor (slim_net_t): bind to
graph/routerand register theclient/listenercatalog types under/net, but DO NOT auto-register the built-in udp/tcp/ws factories.Identical to the default ctor except it omits the
register_builtin_transportscall, so a node that binds its links directly (provide_link) sheds the unused socket transports on a--gc-sectionstarget (see slim_net_t). The composition root registers whatever factories it does want afterward via register_transport_type. Every argument up to the tag is required — the tag keeps its overload-disambiguating position, so the one defaultable argument (egress_src) follows it.- Parameters:
graph – The graph the
/netsubtree is registered on.router – The forwarding router connections are wired into.
net_root – The parent path for connection vertices (e.g. “/net”).
rx_backend – The ADR-0042 §2 RX memory seam — as on the default ctor.
egress_src – The ADR-0079 net-plane EGRESS store this vertex REPORTS through egress_source
(#873). A slim node registers its own factories, so nothing here wires it into a link automatically — but the accessor is the documented way a factory reaches “this net plane’s
store”, and before this parameter existed it answered the process heap on a slim node no matter what the composition root had chosen.
nullptr(and the default) means the process net sub-pool (#1777). Must outlive this object.
-
~transport_vertex_t()¶
Uninstall the routed-subscription hold seam (#1816) before any connection is torn down, so a departure eviction during member destruction never calls back into a half-destroyed plane.
-
void register_transport_type(std::string_view kind, transport_factory_t factory)¶
Register (or replace) the transport factory for config
kindkind.The transport-factory catalog (the kind selector), not the graph’s child-type catalog: a subsequent creator-endpoint SPEC whose config carries
kind = <kind>constructs its transport viafactory. An unregistered kind fails creation withSCHEMA_NOT_FOUND(the same “unsupported catalog entry” convention the graph’s own child-type catalog gives an unknown SPECtype). Call at setup, before frames flow (mirrorsgraph::graph_t::register_child_type’s thread contract).The catalog row is drawn from egress_source (#1780); a store too small to hold it is a sizing bug, so the node stops with a message naming the store (ADR-0056, ADR-0083 Q7), exactly as an exhausted
register_vertexdoes.- Parameters:
kind – The config
kindselector (e.g. “udp”, “quic”), copied.factory – Builds an owning transport from the parsed universal settings plus the raw config TLV (for its kind-private keys).
-
void register_transport_type(std::string_view kind, transport_factory_t factory, transport_kind_traits_t traits)¶
Register a transport factory WITH its kind capabilities (RFC-0014 §4 S5).
The traits overload: identical to the two-argument form, plus the transport_kind_traits_t row that opts the kind’s DIAL connections into the S5 liveness engine. The traits-less overload registers
{}— every existing caller keeps eager construction unchanged.- Parameters:
kind – The config
kindselector (e.g. “udp”, “quic”).factory – Builds an owning transport from the parsed universal settings plus the raw config TLV; with
traits.self_heal_dialset it is re-run once per dial attempt, off the creation path.traits – The kind’s capability row — see transport_kind_traits_t.
-
graph::result_t<void> acquire_link(std::string_view name)¶
A STANDING binding takes its hold on connection
name'slink (RFC-0014 §4).The S5 refcount seam: a standing subscription routed through the link holds it acquired for its lifetime. The routing plane drives this itself (#1816): on a build with the engine, the constructor installs tr::graph::graph_hooks_t::link_hold, so every remote subscription edge that delivers over a connection — one a peer made over it, or one bound toward a target through its mount — calls this when it is admitted and release_link when it is cleared, replaced or evicted. An embedder may still drive it directly for a binding the graph does not see. An
awaittakes no standing hold. While the count is above zero the engine keeps the link’s steady-state targetUP— self-healing on loss withbackoff, forever — and the last release_link closes the socket back toDORMANT. Non-blocking; “bring it up and wait” isawaiton the connection vertex.A connection that is not engine-managed (a provided link, an eagerly-constructed kind, any LISTEN) answers success as a no-op — RFC-0014 §4: a LISTEN link ignores refcount, and a manual link’s liveness stays the caller’s.
- Parameters:
name – The connection’s qualified key
net/<module>/<name>(#605).- Returns:
NotFound if
namenames no created connection.
-
graph::result_t<void> release_link(std::string_view name)¶
The standing binding releases its hold — see acquire_link.
- Parameters:
name – The connection’s qualified key
net/<module>/<name>(#605).- Returns:
NotFound if
namenames no created connection.
-
inline mem::block_source_t &egress_source() const noexcept¶
This net plane’s EGRESS store — the one the built-in factories wire into every socket they construct (see the ctor’s
egress_src, #873 / ADR-0079).Exposed so a factory registered through register_transport_type (the out-of-tree kinds —
quic,can, an embedder’s own) can hand the SAME store to the link it builds, viatr::net::with_egress_source, rather than silently leaving that kind’s gather on the process heap while the built-ins are bounded. A deployer fanning the ADR-0079 NARROW shape ignores this and captures its own per-thread source instead.
-
graph::result_t<void> register_module(std::string_view module, std::string_view kind, conn_role_t role, conn_catalog_t catalog = {})¶
Declare the MODULE that connections of
kindandrolemount under.A creatable *(transport, role)* pair is a self-contained module under the net root (RFC-0014 §1), so a connection vertex lives at
<net_root>/<module>/<name>and its routing key is that same path (ADR-0061). Modules are declared-only, by the application (ADR-0073 §4): the library never derives or auto-registers a module name, so every module segment in the graph traces to an application decision. The mapping is not uniform either: a transport with both a dial and a listen shape is TWO modules (ws-client,ws-server), while a bus likecanis ONE for both roles — “a
bus has no dial/listen asymmetry” (
transport_can.hpp).Built-in transports ship suggested module names (e.g.
kWsClientSuggestedModuleintransport_ws.hpp) the application may pass here — but the call is always application code. Registration is a minting boundary, so the shared segment-validity predicate (ADR-0073 §1) gatesmodule:a name carrying a reserved character, empty, or over the segment byte cap answersINVALID_PATHand registers nothing.Declaring a module MINTS its creator endpoint
(RFC-0014 §1: “adding a module adds
its creator endpoint and catalog”): this registers the
<net_root>/<module>grouping vertex and, below it, the write-driven<net_root>/<module>/connendpoint whoseSPEC/NAMEwrites create and remove the module’s connections. Both are idempotent — a second declaration naming the same module (a module serving two kinds) finds them and mints nothing. The endpoint is minted HIDDEN from the module’s:children[](RFC-0014 §3, S4) — seekConnEndpointName.The module may declare its creation CATALOG here (
catalog, RFC-0014 §2 / Amendment 3) — the config keys aSPECto its endpoint may carry, kind-private ones included, and the shape each must take. It is whatread <net_root>/<module>/conn:schemaanswers inside itsSETTINGS, and the endpoint REFUSES,TYPE_MISMATCHand before anything is built, aSPECwhose config omits arequiredkey or carries a catalogued key in any other shape than the declared one (the config “malformed” of §2). Keys the catalog does not name stay the forward-compatible unknown pairs they always were — ignored, never refused. With no catalog (the default) nothing changes: the endpoint answers Amendment 3’s emptySETTINGSand validates nothing, and the graph stores nothing for it. The catalog is the MODULE’s — one per endpoint — so it is fixed by the declaration that mints the endpoint: a later declaration under the same module may pass the same table again or none, and any other one is refusedPATH_IN_USE.A *(kind, role)* pair is declared exactly once. The declaration is KEYED on that pair — module_for resolves through it — so re-declaring an already-declared pair under a DIFFERENT
moduleanswersPATH_IN_USEand changes nothing. It used to overwrite the recorded name in place, which left the first module’s creator endpoint minted and answering while every later resolution pointed at the second name: two live doors, one of them unreachable through the resolver, and no diagnostic. Declaring the SAME triple again is idempotent and still succeeds. One module may serve several pairs (acanmodule for both roles, a module declared for two kinds); what is refused is one pair claimed by two modules.- Parameters:
module – The module segment (e.g.
"ws-client","can"); must satisfytr::graph::valid_segment.kind – The config
kindthis module constructs (e.g."ws").role – The role this module fixes positionally.
catalog – The module’s creation catalog, BORROWED for this object’s lifetime; empty (the default) declares none.
- Returns:
INVALID_PATHifmoduleis not a valid path segment;PATH_IN_USEif *(kind,role)* is already declared under another module, orcatalogcontradicts the onemodule'sendpoint was minted with (nothing is minted and nothing is recorded); the graph’s own refusal (e.g.BACKPRESSURE) if the endpoint could not be registered — in which case nothing is declared either.
-
graph::result_t<std::string_view> module_for(std::string_view kind, conn_role_t role) const¶
The module a connection of
kindandrolemounts under (RFC-0014 §1).Declared-only (ADR-0073 §4): a *(kind, role)* pair the application never declared via register_module answers
SCHEMA_NOT_FOUND— the unsupported-catalog-entry convention an unknown SPECtypeuses — instead of a library-derived name.Note
Thread-safe (#881): this takes the control mutex, so it may run concurrently with register_module and with wire-driven connection creation. It used to read the declaration vector with no lock, which a concurrent register_module could reallocate out from under the walk — the declare-only-at-setup contract that papered over the gap is WITHDRAWN. The lock is control-plane; nothing on the forward or delivery path takes it.
- Returns:
A view of the declared module name. It stays valid for this object’s life: a declaration is never removed and its text never moves (#1780).
-
bool is_structural(wire::key_view_t key) const¶
Is
keyone of THIS net plane’s structural vertices — the net root, or a<net_root>/<module>segment — rather than a connection or an application vertex (#1096)?transport_vertex_tmints two vertices nobody asked for: the net root (the:children[]ENUMERATION root) and, lazily, each<net_root>/<module>segment a connection mounts under. Both are registeredrole_t::STORED_VALUEand carry no descriptor table, so an embedder walking graph::graph_t::for_each_vertex sees them as ordinary value vertices someone forgot to describe — byte-identical:schemashape to a real leaf, differing only in the NAME. This predicate is how an embedder tells them apart without re-typing the library’s own naming rule.The answer is deliberately scoped HERE and nowhere wider.
graph_tcannot answer it: an application’s own structural vertex (a/zoneholding nothing but children) is indistinguishable from a connection vertex on every graph-visible surface — same visit, same schema shape, same composed-branch read (RFC-0016) — so a graph-level predicate would be inventing an answer. What the LIBRARY minted, the library can report; what the APPLICATION minted stays the application’s business (ADR-0010: libtracer is a transport for application data, not a definer of application semantics). Seedocs/reference/11§structural vertices.Note
Name match, not provenance — a documented false positive. Creation deduplicates against
graph_.find, deliberately keeping no per-module minted set (commit221ed983deleted exactly that state). So a vertex an application registered at<net_root>or<net_root>/<module>before this object got there answerstruehere: the predicate says “this key names a structural
position of this net plane”, not “this object registered this vertex”.
Note
The RFC-0014 per-module creator endpoint
<net_root>/<module>/conn(implemented by S2b) is not structural and answersfalse: it is an addressable control surface — arole_t::HANDLERvertex that EXECUTES theSPEC/NAMEwrites reaching it — not a grouping segment, and it sits one level below the deepest structural position anyway.- Parameters:
key – The canonical PATH-payload key
for_each_vertexhands its callback (no handle unwrapping needed — the signature matches).- Returns:
true iff
keyis<net_root>, or<net_root>/<module>for a module of this plane — one declared through register_module, one staged through provide_link (which takes the module as data and declares nothing, so a staging can name a module no declaration ever did), or one carrying a live connection.
-
void provide_link(std::string_view module, std::string_view name, transport_t &link)¶
Supply a pre-built transport a subsequent SPEC of connection
namebinds.The test/manual seam: the link is not constructed from the config — it is handed in here (a loopback endpoint, a test channel, a transport the catalog doesn’t cover) and wired into the router when the matching creator-endpoint SPEC is created. The caller keeps ownership. Call at setup, before the SPEC write.
The staging key is
<module>/<name>in BOTH halves (#883). A creating SPEC reaches this staging when it resolves to the same module — i.e. it carries nokind(and no second staging sharesname), or it carries akindwhose register_module declaration for the creation’s role namesmodule. Then the staged link takes precedence over config construction, and thekind’s factory does not run. Akinddeclared under a DIFFERENT module builds its own socket there and leaves this staging untouched — it no longer captures the creation by leaf NAME alone.
The staging row is drawn from
egress_source; exhaustion at this setup call stops the node with a sizing message, as register_transport_type does (#1780).- Parameters:
module – The module the connection mounts under (
/net/<module>/<name>). Required because a staged link may bypass the transport factory, so there is nokindto derive one from — the caller staging the link says where it mounts (RFC-0014 §1).name – The connection’s NAME (the
/net/<module>/<name>leaf segment).link – The transport carrying this connection’s bytes.
-
graph::result_t<void> set_link_state(std::string_view name, link_state_t state)¶
Report a connection’s link-liveness state — a write to the vertex value.
Writing the 1-byte link_state_t makes
await(/net/<name>)fire (ADR-0021:awaitis the vertex’spoll) and delivers to every subscriber (RFC-0008 §D assign-then-deliver). Eagerly-constructed transports are setUP/LISTENINGat creation; this remains the seam for later link events (and the only source for provided links). On an ENGINE-MANAGED connection (aself_heal_dialkind’s DIAL, RFC-0014 S5) the engine is the sole writer of the DIAL transitions — a manual write here still lands but is advisory-at-best and the next transition overwrites it; drive such a link through acquire_link / release_link instead.Note
Thread-safe (#881): this takes the control mutex, so the transport thread reporting a provided link’s liveness may run concurrently with the wire-driven create/remove that inserts into and erases from the connection table. It used to look the connection up with no lock, racing that map’s rebalance and the erase of the very node it returned. The vertex write happens inside the locked section, in the order the class documents (this → router → graph → stripe).
- Parameters:
name – The connection’s qualified key
net/<module>/<name>(#605).state – The link-liveness state to publish (see link_state_t).
- Returns:
NotFound if
namenames no created connection vertex.
-
graph::result_t<void> remove_connection(std::string_view name)¶
Remove connection
name— un-route it, retire its vertex, close its socket.The teardown counterpart of creation (#494), in the order the invariants require:
fwd_router_t::remove_childfirst (the name stops resolving, so no forward can reach the link), thengraph.retire()on the identity vertex (RFC-0009 §B.6 — the path re-virginizes), then the owned transport is destroyed (joining its recv thread). A connection whose link was staged via provide_link leaves that borrowed link alone; only the routing entry and the vertex go.This is the owner-internal operation the RFC-0014
NAME-write removal dispatch (S2b) will call; it is not itself reachable from the wire.- Parameters:
name – The connection’s qualified key
net/<module>/<name>— NOT the bare connection NAME. RFC-0014 S2a re-keyedconns_to the qualified form so the routing address equals the vertex path; these lookups moved with it and the doc did not, so a caller following the old wording got a silentNOT_FOUND/nullptr(#605).- Returns:
NotFound if
namenames no created connection.
-
const conn_settings_t *settings_of(std::string_view name) const¶
The parsed transport-private settings of connection
name(nullptr if none).The record’s text keys view this connection’s own copy of its config, so they live as long as the connection. The POINTER is valid until the next connection is created or removed: the table is one sorted block (#1780), and growing or shrinking it moves the records. Read what you need and drop it.
- Parameters:
name – The connection’s qualified key
net/<module>/<name>(#605).
-
transport_t *link_of(std::string_view name) const¶
The OWNED transport of connection
name— the config-constructed socket.The seam for reaching a SPEC-constructed listener/server after creation (e.g. to enumerate its peers via
link_of(name)->bus()or close one vialink_of(name)->bus()->close_peer(peer)). Returns nullptr for a connection whose link was staged with provide_link (the caller already owns that link) and for an unknown NAME.- Parameters:
name – The connection’s qualified key
net/<module>/<name>— NOT the bare connection NAME. RFC-0014 S2a re-keyedconns_to the qualified form so the routing address equals the vertex path; these lookups moved with it and the doc did not, so a caller following the old wording got a silentNOT_FOUND/nullptr(#605).
-
struct conn_settings_t¶
One connection’s transport-private settings — a
tr::netrecord, not part of any vertex’s protocol:settingssurface.addr/port/role/kindare a device-private:settingsfacet of a connection vertex (ADR-0021: standard vs device-private fields), so they live here on thetr::netleaf record. They are reached through this transport’s own config door, never through the vertex:settingscore namespace — which RFC-0022 §3.B emptied outright, so there is no shared per-vertex policy record for these to be confused with or to leak into.kindselects the transport factory (e.g."udp","ws") used to construct the socket when no transport_vertex_t::provide_link was staged; empty = pre-supplied link only.This record carries ONLY the universal keys every transport kind shares (the ADR-0043 §5 leanness ruling): a kind’s PRIVATE config (e.g. quic’s
tlsprofile name) never lands here — the kind’s own factory parses it from the raw config SETTINGS TLV it receives alongside these settings. A universal key with no consumer is not kept here either: thekeepalivekey is still ACCEPTED on the wire (existing configs parse) but is ignored and lands nowhere (#1666) — a keepalive a kind needs belongs in that kind’s own config.The two text keys are VIEWS, not owned strings (ADR-0083 Q15, #1780): they borrow the config bytes they were parsed from.
transport_vertex_tparses them out of its own copy of the SPEC’s config, which lives as long as the connection, so a factory, the liveness engine and transport_vertex_t::settings_of all read views that stay valid for the connection’s life. A caller building a record by hand keeps the viewed text alive for as long as the record is used.Public Members
-
std::string_view addr¶
Peer IPv4 dotted-quad (DIAL).
-
std::uint16_t port = 0¶
Peer port (DIAL) / bind port (LISTEN);
0on a LISTEN is the EPHEMERAL request — see port_set.
-
bool port_set = false¶
Was a
portkey PRESENT in the config? Distinguishes an omitted required key (aTYPE_MISMATCHconfig error) from an explicitport = 0, which on a LISTEN asks the OS to pick the bind port (#1362). Read the granted port back off the constructed link (local_port()).
-
conn_role_t role = conn_role_t::DIAL¶
Link direction. POSITIONAL: set from the module’s own
register_moduledeclaration, never from the config. Theroleconfig key that once overrode it died with the:children[]door at RFC-0014 S7.
-
std::uint32_t max_frame = 0¶
Per-connection receive frame cap for every framed transport — the length-prefix streams (
tcp,quic,webtransport) read it off their u32 prefix,wsoff the RFC 6455 frame header; 0 = the protocol default (kMaxFrame, 16 MiB). Only tightens, never raises.
-
std::string_view kind¶
Transport-factory selector (“udp”, “ws”, …); empty = provide_link only.
-
std::uint32_t backoff_ms = 0¶
DIAL self-heal retry interval (RFC-0014 §4); consumed by the S5 liveness engine (
), 0 = the engine’s default (self_heal_link.hppkDefaultBackoffMs).
-
std::uint32_t connect_timeout_ms = 0¶
DIAL connect-attempt deadline (RFC-0014 §4): how long one dial waits for
UPbefore it counts as failed; consumed by the S5 engine. 0 =kDefaultConnectTimeoutMs.
-
std::string_view addr¶
-
using tr::net::transport_factory_t = inline_fn_t<graph::result_t<transport_ptr_t>(const conn_settings_t&, const wire::tlv_node_t*, mem::block_source_t&), kTransportFactoryCapacity>¶
Constructs an owning transport from a connection’s parsed settings plus the raw config TLV, in a block from the source it is handed.
The shared conn_settings_t carries ONLY the universal keys (the ADR-0043 §5 leanness ruling); the raw config is the SPEC’s config SETTINGS TLV as written (may be null when the SPEC carried none), from which a kind’s factory parses its own kind-private keys (e.g. quic’s
tls/insecure) — the factory’s business, module-side. The third argument is the store the link object itself is drawn from: the receivingtransport_vertex_tpasses its own (receiver pays, ADR-0083 Q21), and the factory builds withmake_transportover it. Its refusal isBACKPRESSURE.Returns the live transport, or a status:
TYPE_MISMATCHfor a config missing the fields the kind requires (e.g. a DIAL withoutaddr/port),TRANSPORT_DOWNfor a socket that failed to come up (bind/dial/handshake failure).The did-not-come-up status is
TRANSPORT_DOWN, notNOT_FOUND(#929), and a factory written outside the library owes the same answer: the address the SPEC named RESOLVED — the failure is the LINK — andNOT_FOUNDgoes out astr::path::not_found, which the RFC-0002 registry marks PERMANENT, telling a peer to stop retrying a link that may well come back.TRANSPORT_DOWNcarries the TRANSIENT disposition the condition actually has.A stored, non-hook callable, so it is an
inline_fn_t(ADR-0083 Q10/Q18): the captures live inline, and one that does not fit, or owns a resource, fails to compile.
-
enum class tr::net::conn_role_t : std::uint8_t¶
The connection’s transport-private role (ADR-0027 §default link direction).
DIAL= this node opens the link (the consumer-dials default);LISTEN= this node accepts. A config-constructed socket transport acts on it (bind vs. connect).Values:
-
enumerator DIAL¶
-
enumerator LISTEN¶
-
enumerator DIAL¶
-
struct slim_net_t¶
Tag selecting the SLIM transport_vertex_t ctor — the one that does NOT auto-register the built-in udp/tcp/ws transport factories.
The default ctor is batteries-included: it registers the built-in socket factories so a full node can create udp/tcp/ws connections from a SPEC
kind. A slim node binds its links DIRECTLY instead — transport_vertex_t::provide_link stages a hand-constructed transport and a creator-endpointSPECwires it in (the way the device/VB nodes stage their ws and CAN links) — so it never routes creation through the built-in factories. Yet while the ctor hard-referencesregister_builtin_transports, the linker must keep udp+tcp+ws (and their factory glue) even on a node that binds none of them: constructing the/netvertex pulls all three in. Passing slim_net_t selects a ctor whose translation-unit graph never namesregister_builtin_transports, so on a--gc-sectionstarget the unbound factories — and the transport TUs nothing else references — garbage-collect. A slim node re-adds exactly the factories it wants via transport_vertex_t::register_transport_type (or a hand-picked register_*_transport).NON-BREAKING: the default (full-node) ctor is unchanged, so existing consumers keep the auto-registered builtins; only a node that opts in with this tag sheds them. Compile-time (not a runtime flag) precisely so the reference is absent from the slim TU and the GC can fire.
See also¶
transport — the wire seam — what
fwd_router_tsends through.graph — vertices & dispatch — the terminus the router resolves against.
interface map — the net plane in the cross-cutting view.
Two nodes over a wire — a runnable two-node forward and reply.
Reference 13 — network formation and Reference 04 — communication flows — the same model described implementation-independently.
Failable allocation and backpressure — the drop rule and the block-source seam.
Concurrency and scaling — where this implementation’s measured costs live, with their conditions.