graph — vertices, read/write/await, dispatch (L4)¶
In one paragraph
The graph is the node. A vertex_t is a named, addressable slot holding a value
(a rope_t — a contiguous scalar is the single-link case), a bounded history, or a
user handler. The data surface is read / write / await over the value, plus
assign / propagate when the state transition and the edge transition are wanted
separately; every control surface (subscriptions, QoS) is a field-write to a
:-addressed field. write fans out to subscribers by cloning the value (a refcount
bump, no copy). The last-known-value path takes no per-vertex mutex.
What it does¶
graph_t owns the vertex map (keyed on canonical path bytes). Each vertex
has a role: stored-value (last-writer-wins), stream (the CONSUMER’s bounded ring —
a producer never queues, so the ring lives on the receiving vertex and is bounded in BYTES
by that vertex’s own injected mem::block_source_t via vertex_policy_t::ring_source
(RFC-0025 §4.6.1) — whose depth the
owner declares host-side with vertex_policy_t::retention, and which no peer can read or write —
RFC-0022 §3.C), or handler (on_read / on_write — covering
computed, proxy, sink, live-MMIO patterns). The last-known-value slot is a one-word
value_t* swap under the slot policy (RFC-0028 slice 3), so read / write of the value take no
per-vertex mutex; that mutex guards the subscriber list, the history ring and the
await waiter accounting, and a per-vertex condvar makes await block until the next
write.
What a vertex keeps after a write is one policy, retention_t { NONE, LAST, N }
(RFC-0028 §5.4),
declared owner-side with vertex_policy_t::retention. Each role has a default — a handler keeps NONE, a
stored value LAST, a stream N — and a stored value or a stream may be declared NONE: the
pure relay, which delivers every write to its subscribers, keeps nothing, and answers read
with NOT_FOUND. A relay whose subscribers are all callbacks delivers from the writer’s stack
and draws no block at all. App fields carry the same enum: a wo field is NONE, so its
write reaches on_app_field_write and is stored nowhere.
The slot is a build-time policy. By default it is single_writer_slot_t, whose one wait
is a guard that never spins (#1618): a striped mutex on a host, an interrupt-masked
section on a chip. A host can opt into hazard_slot_t, which is lock-free but still pays
one contended increment to promote a read into an owning handle. The claim the code supports is the mutex one, not an absence of
contention; the costs are in design/concurrency.
Subscriptions are field-writes, not a verb
(ADR-0006 — read/write/await API, no connect):
subscribing is writing a SUBSCRIBER TLV into :subscribers[]. On each write the
dispatcher clones the value to every subscriber’s target vertex and in-process callback.
A delivery terminates at its target — store and notify, never a re-dispatch to the
target’s own :subscribers[] — so a dispatch-level cycle cannot form and there is no
depth cap to tune (core/include/libtracer/graph.hpp:There is no in-process;
ADR-0051 — delivery terminates at target, no dispatch limits,
RFC-0007 — delivery terminates at target).
Propagation past a target is exclusively the target’s own logic — a controller
re-emitting on its execution. :schema reads return a POINT descriptor.
Retention is vertex state. A subscription’s latch (durability_request) and its enabled bit are edge state, separate from retention.
Interface¶
enum class role_t { STORED_VALUE, STREAM, HANDLER };
enum class retention_t { NONE, LAST, N }; // RFC-0028 §5.4 — per vertex AND per app field
enum class delivery_mode_t { IF_NEWER, UNCONDITIONAL, EXPLICIT };
// There is NO per-vertex settings type. RFC-0022 §3.B deleted `settings_t` outright: four
// of its seven knobs were inert, `durability` became the subscription's (below), and the two
// survivors are construction parameters an OWNER declares — see vertex_policy_t below
// (RFC-0028 §4.12, D12). Nothing is inherited (§3.F).
struct delivery_policy_t { // ONE subscription's delivery policy (RFC-0022 §3.A) — 2 B packed
std::uint16_t bits; // 0-1 reliability | 2-4 priority | 5 durability_request | 6-15 rsvd
};
struct write_ctx_t { // the per-call context a write carries into on_write (#375)
std::string_view subject; // the writer's resolved subject token; EMPTY = the local host
bool is_local_owner() const noexcept; // subject.empty()
const net::link_kind_t* link; // the arrival link's catalog (kind, role); null = none (#1650)
}; // BORROWED for the call, like the value beside it — copy if retained
template <class R, class... A>
struct hook_t<R(A...)> { // THE callback idiom (RFC-0028 D10): {fn, ctx}, 16 B, owns nothing
R (*fn)(void* ctx, A...);
void* ctx; // caller-owned; must outlive the vertex
};
thunk(f); // hook over a callable you keep alive (a stateless one: any lifetime)
struct handlers_t { // six hooks, 96 B on the host
hook_t<result_t<value_ref_t>()> on_read; // one read type
hook_t<result_t<void>(const value_t&, const write_ctx_t&)> on_write; // BY REFERENCE
hook_t<result_t<view_t>()> on_children;
hook_t<admission_t(const value_t&, const write_ctx_t&)> on_admit;
hook_t<result_t<view_t>(std::string_view, const view_t&, const write_ctx_t&)>
on_app_field_admit;
hook_t<void(std::string_view, const view_t&)> on_app_field_write;
}; // keep a value past on_write/on_admit: value_ref_t::keep(value)
using subscriber_fn_t = void (*)(void* ctx, const value_t& value);
class subscription_t { /* opaque: producer vertex + :subscribers[] slot index; graph_t is the
sole friend. Public: default-construct, copy, operator==. */ };
template <class Fn> struct graph_hook_t { Fn fn; void* ctx; }; // one graph-wide seam
struct graph_hooks_t { // the five graph-wide seams as ONE aggregate (RFC-0028 D9)
graph_hook_t<subject_resolver_fn_t> subject_resolver; // ACL enforcement switch
graph_hook_t<sub_observer_fn_t> subscription_observer;
graph_hook_t<remote_delivery_fn_t> remote_delivery; // the router installs it
graph_hook_t<wire_target_fn_t> wire_target; // the router installs it
graph_hook_t<stats_sampler_fn_t> stats_sampler; // the router installs it
};
struct vertex_policy_t { // everything the OWNER declares about one vertex (RFC-0028 D12)
std::optional<retention_t> retention; // unset ⇒ the role's default (§5.4)
std::uint32_t depth = 1; // ring depth under retention_t::N
std::size_t share_threshold_bytes = kShareThresholdBytes; // §5.3
mem::block_source_t* ring_source = nullptr; // RFC-0025 §4.6.1; null ⇒ graph default
bool ring_reliable = false; // §4.4 pressure arm
delivery_mode_t delivery_mode = delivery_mode_t::IF_NEWER;
app_fields_decl_t app_fields; // owning OR borrowed (RFC-0010 §A)
};
class graph_t {
explicit graph_t(mem::block_source_t& src = mem::heap_source(), graph_hooks_t hooks = {});
// registration and removal — the policy is applied WHOLE, at registration
vertex_handle_t register_vertex(const path_t&, role_t, handlers_t = {},
vertex_policy_t policy = {},
std::span<const payload_right_t> rights = {});
result_t<vertex_handle_t> try_register_vertex(const path_t&, role_t, handlers_t = {},
vertex_policy_t policy = {},
std::span<const payload_right_t> rights = {});
result_t<void> retire(vertex_handle_t); // logical absence, subtree-wide
std::uint32_t retire_generation(vertex_handle_t) const noexcept;
void collect(); // free the parked value seams — CALLER-timed
std::size_t parked_seam_count() const; // how many await a collect()
std::optional<vertex_handle_t> find(std::span<const std::byte> key) const;
// the node-scoped vertex index — bound-path addressing (RFC-0024 §6.4)
std::size_t vertex_slot_count() const noexcept;
std::optional<vertex_slot_t> vertex_slot(vertex_handle_t) const noexcept; // mint side
std::optional<vertex_handle_t> deref_vertex_slot(std::uint32_t index,
std::uint32_t generation) const noexcept;
std::optional<vertex_slot_t> vertex_slot_at(std::uint32_t index) const noexcept; // O(1)
bool allows(vertex_handle_t, std::string_view caller, acl_right_t) const; // the §6.2 check
// value plane
result_t<value_ref_t> read (vertex_handle_t, std::string_view caller = {}) const;
result_t<void> write(vertex_handle_t, rope_t, std::string_view caller = {},
const net::link_kind_t* link = nullptr); // #1650
result_t<value_ref_t> await(vertex_handle_t, std::chrono::nanoseconds,
std::string_view caller = {});
result_t<void> assign(vertex_handle_t, rope_t, std::string_view caller = {});
result_t<void> propagate(vertex_handle_t);
result_t<std::size_t> history(vertex_handle_t, std::span<value_ref_t> out) const; // 0 allocs
result_t<value_ref_t> read (vertex_handle_t, const field_path_t&, // ONE read type
std::string_view caller = {}) const; // (RFC-0028 D11)
// RFC-0008 §E drain cursor — what the stream OWES, and how to say it is paid
result_t<std::size_t> drain_unflushed(vertex_handle_t,
std::vector<value_ref_t>& out,
std::uint64_t* gap_before = nullptr);
result_t<void> mark_flushed(vertex_handle_t);
// owner-side declarations (RFC-0022 §3.C) — host API only, NO wire surface. ONE verb:
// the policy is stated whole, so a member left at its default resets that property.
result_t<void> set_policy(vertex_handle_t, vertex_policy_t); // SCHEMA_NOT_FOUND if illegal
retention_t retention (vertex_handle_t) const noexcept;
result_t<std::size_t> ring_reserved_bytes(vertex_handle_t) const;
result_t<std::uint64_t> stream_gaps (vertex_handle_t) const;
std::size_t share_threshold_bytes (vertex_handle_t) const noexcept;
// graph-wide seams — read-modify-write, for a party wired after construction (the router)
void set_hooks(const graph_hooks_t&) noexcept;
graph_hooks_t hooks() const noexcept;
// composed reads — they build a value, so they return a reference to a fresh block
result_t<value_ref_t> read_children_folded(vertex_handle_t) const;
result_t<value_ref_t> read_children_materialized(vertex_handle_t) const;
result_t<value_ref_t> read_subtree_folded(vertex_handle_t, ...) const;
// field plane (`:`-addressed)
result_t<value_ref_t> read (vertex_handle_t, const field_path_t&, ...) const;
result_t<void> write(vertex_handle_t, const field_path_t&, rope_t,
std::string_view caller = {});
result_t<value_ref_t> read (const path_t&) const; // field tail → :schema, …
result_t<void> write(const path_t&, rope_t); // → :subscribers[], :settings.*
result_t<value_ref_t> await(const path_t&, std::chrono::nanoseconds);
// subscriptions
result_t<void> subscribe(const path_t& src, const path_t& target,
delivery_policy_t policy = {});
result_t<subscription_t> subscribe(const path_t& src, subscriber_fn_t fn, void* ctx,
delivery_policy_t policy = {});
template <typename F>
result_t<subscription_t> subscribe(const path_t& src, F& callback); // lvalue only
result_t<void> unsubscribe(const subscription_t&);
result_t<void> unsubscribe(const subscription_t&, subscriber_release_fn_t);
static std::uint64_t deferred_release_drops() noexcept;
};
There is no std::function anywhere on the graph’s seams (RFC-0028 D10): every hook — the
subscription edge, the six handlers_t seams, the child-type catalog — is a {fn, ctx} pair,
and a caller keeps the ctx alive. A seam that is handed const value_t& (a callback edge,
on_write, on_admit) borrows it for the call; to keep it, take value_ref_t::keep(value),
which shares a published block and copies the links out of a relay’s stack storage. There is
no result_t<void> callback form. The per-edge sink is a {fn, ctx} pair so the per-publish edge snapshot under the fan-out lock
is a trivial copy rather than a std::function clone that heap-allocates once captures
exceed the small-buffer size
(ADR-0047 — build-time closed module sets, compile-time seams).
The templated overload binds callback by address, so it takes an lvalue only — a
temporary lambda does not compile.
```{admonition} ctx lives until the reclamation policy’s grace point — and the library tells you when
:class: important
unsubscribe deactivates the slot (core/include/libtracer/graph.hpp:graph_t::unsubscribe(const subscription_t& sub)); a
delivery already in flight snapshotted the edge and completes, and the {fn, ctx} pair is
the one leg of that snapshot the library owns no copy of. So “when may I free ctx?” is answered by this build’s reclamation policy
(ADR-0080,
reference/17), not by a rule you have to keep in
your head — and never by asking you to track in-flight state.
Under the default reclaim_local, pass a release hook and be told:
void on_dead(void* ctx) { delete static_cast<my_sink_t*>(ctx); }
(void)g.unsubscribe(sub, &on_dead);
The hook runs exactly once, on your thread, outside every graph lock: inline, before
unsubscribe returns when you called it from outside a delivery (the ordinary case), or
before the enclosing write() returns when you called it from inside one. The
one-argument overload retires the edge identically and simply carries no signal — which is
sufficient whenever you unsubscribe from outside a callback, since that call is already
quiescent on return (core/include/libtracer/graph.hpp:@param ctx Caller-owned states the bound on ctx).
```{admonition} No strings on the hot path
:class: important
The hot path is **handle-typed** (the spec's rule,
[reference/03](../reference/03-addressing.md) §static path handles).
A `path_t` encodes the canonical PATH bytes **once** — the `path_t(std::string_view)`
constructor for a known-good literal
([ADR-0054 — path_t parse-once constructor](https://github.com/avatarsd-llc/libtracer/blob/main/docs/adr/0054-path-t-parse-once-constructor.md)),
or the fallible `path_t::parse` for a runtime string; `register_vertex` / `find` resolve a
**`vertex_handle_t`** once; then `write(v, value)` and `write(v, fieldpath, value)` reuse
those handles — **no string crafting, no parse, no map lookup per call**. The
string/`path_t` overloads are init-time conveniences.
Injected memory — no allocator baked in
graph_t’s constructor takes three memory seams, all defaulted to the standard heap (a
host that passes nothing gets zero-churn, byte-identical behavior):
a
std::pmr::memory_resource*for the per-write control objects — the LKV control block and therope_twrapper (ADR-0039 — pmr memory model, host-aligned allocation);a
mem::mem_backend_t* value_backendfor the durable value bytes the write path copies into the LKV when a borrowed-delivery transport forces the copy (ADR-0060 — LKV copy store, injected value backend); anda
mem::block_source_t* ctl— the nothrow source for allocations a peer can provoke, which report exhaustion by value instead of throwing (ADR-0065 — failable allocation gets its own seam, block_source). Read it back withcontrol_source().
The parameters are appended in that order, so an existing graph_t{&mr} keeps
compiling and picks up the defaults.
A bounded target points all three — and the transport-receive backend — at one static slab;
pool exhaustion surfaces as BACKPRESSURE, never a silent heap fallback. See
reference/09 §the injection points.
// idiomatic: encode the path once (parse-once ctor), reuse the handle
path_t p("/x:settings.app.setpoint"); // once — no *-deref
auto v = *g.find(p.key()); // once — find → optional<vertex_handle_t>
for (...) g.write(v, p.field(), setpoint_tlv); // hot loop — zero strings
What a read hands back¶
read and await return result_t<value_ref_t>, not result_t<rope_t>
(core/include/libtracer/graph.hpp:graph_t::read(vertex_handle_t v, std::string_view, core/include/libtracer/graph.hpp:graph_t::await(vertex_handle_t v, by handle, core/include/libtracer/graph.hpp:graph_t::read(const path_t& path), core/include/libtracer/graph.hpp:graph_t::await(const path_t& path, by path;
value_ref_t at core/include/libtracer/value.hpp:value_ref_t). A value_ref_t is an owning
reference to the value the vertex published: the LKV slot holds one intrusive value_t*
— a refcount, the link count and the link chain in a single block drawn from the vertex’s
block_source_t — so handing that reference back costs one refcount increment instead of
one segment_ptr_t clone per link.
The rule, and the reason the API is not uniform:
A read of a published value returns a reference to it; a read that composes a new value returns the value.
read_children_folded, read_children_materialized and read_subtree_folded compose a
tree no vertex ever published, so there is no object to reference and they still return
result_t<rope_t>. The field-read overload likewise serves a control TLV as a rope_t.
The rejected alternative was a uniform rope_t return: it makes every read of a shared
vertex pay a contended refcount read-modify-write per link, on a cache line every reader
of that vertex shares, so its cost grows with links and with readers.
Spelling a read of a single-link value:
auto got = g.read(v); // result_t<value_ref_t>
if (!got) return got.error();
std::span<const std::byte> b = (*got)->only().bytes(); // (*got) → const rope_t&
operator* yields the rope_t, operator-> reaches its members; only() is the
single-link accessor (zero copy) and materialize() the general one. (*got)->only()
is the correct spelling — one dereference for the result_t, one for the reference.
A held reference pins the value
Holding a value_ref_t keeps the value alive, exactly as the reader’s own copy did. Under
an injected block_source_t that is a real obligation rather than a
formality: the value’s block was drawn from the graph’s source, so an outstanding reference
pins that allocation and defers its reclamation
(ADR-0069 — LKV slot is a compile-time policy, hazard reclamation).
A reader that parks a value_ref_t in long-lived state holds a bounded pool’s block for
that long.
A published value_t is one block holding a refcount and the written rope’s links, not the bytes they point into: it is neither a rope_t nor a segment. The value is the storage form and a rope stays the write and egress form; a sink that needs a rope clones the links with value_t::rope.
Assign and propagate¶
write is not irreducible. It is assign — the state transition — followed by
delivery — the edge transition
(RFC-0008 — vertex operations, assign and propagate §D).
Splitting them is what makes “update many fields, notify once” expressible without a
notion of a batch:
Call |
State |
Edges |
|---|---|---|
|
swaps the LKV, appends to the stream ring, bumps the write sequence (waking |
none |
|
none |
delivers |
|
as |
delivers immediately |
assign is WRITE-gated like write and is never gated by delivery_mode. A branch
POINT decomposes and assigns each descendant, notifying nothing. propagate takes no
value — it reads the last-known-value — and always delivers the vertex named, because that
vertex is the explicit target; the mode gates only what an ancestor’s sweep sweeps up.
Its cost is O((pending + unconditional) in subtree).
Both verbs require RETENTION (RFC-0008
Amendment 2). A HANDLER vertex retains nothing — it hands the value to on_write and stores
no LKV — so the pair had nothing to carry between the two calls and the sweep delivered silence.
assign(v, …) and propagate(v) therefore answer SCHEMA_NOT_FOUND when v’s role retains
nothing (the contract-mismatch status, not BACKPRESSURE); use write, which dispatches the
seam and delivers in one step. Only the sweep root is judged. The same amendment makes
await serve its woken value through the same role dispatch read uses, so a handler
vertex answers an await with its on_read-composed value instead of NOT_FOUND.
vertex_policy_t::delivery_mode sets that per-vertex policy, declared at register_vertex or
through set_policy. It is a wiring-time host declaration, stated alongside retention, the
share threshold, the ring source and the app field table — an owner declaration with no wire
surface.
|
An ancestor’s sweep includes this vertex |
|---|---|
|
only if it was assigned since the last covering sweep — the structural coalescing flush |
|
always, at the sweep’s rate — a sweep-driven keepalive |
|
never; deliverable only by a direct |
The mode is a structural filter, not a value filter: nothing here compares bytes, and a vertex never parses its own bytes. Numeric filtering (a deadband) is an application filter vertex, never a field here. The protocol half of this model — what a peer sees, and how coalescing composes across a link — is reference/02.
Write and fan-out¶
sequenceDiagram
participant P as publisher
participant G as graph
participant V as /sensor/temp
participant S1 as subscriber (callback)
participant S2 as subscriber (target vertex)
P->>G: write(/sensor/temp, rope_t)
G->>V: atomic LKV store (no per-vertex mutex)
G->>V: snapshot subscribers (brief lock)
G-->>S1: fn(ctx, clone) %% refcount bump
G-->>S2: store + notify at the target %% no re-dispatch from there
Note over V: await waiters woken via condvar
G-->>P: OK
The subscriber snapshot is taken under the per-vertex mutex and the sinks are called outside it, so a callback may re-enter the graph. Because a delivery landing on a target does not re-fan from that target, re-entry cannot build a dispatch cycle.
A remote subscriber’s delivery does not go on the wire from here: the fan-out hands
{link, return_route, delivery_compact} and the value to the graph’s injected
remote-delivery sink, which is a tr::net concern. See
fwd-router and transport.
Status codes¶
status_t (core/include/libtracer/status.hpp:status_t) is the error side of every
result_t. When the operation arrived over the wire, the FWD resolver maps it to the
registered tr:: error code the kind=ERROR reply carries (error_code(status_t),
core/src/fwd_reply.cpp:error_code — a private TU under src/, not part of the
public API).
The table below is a total map, and the compiler keeps it that way: error_code is a
switch with no default: label and no fall-through tail, compiled under -Werror=switch,
so a status_t gained without a row here is a red build rather than a status that goes out
under some other member’s wire code. It reads as a formality only until you notice that the
two enums are deliberately separate registries — status_t is L4 vocabulary, err_t is the
wire’s — which is what makes the mapping hand-written and therefore losable.
|
Wire error |
What produces it |
|---|---|---|
|
|
the path resolves to no live vertex (never registered, or retired), or the vertex holds no last-known-value yet |
|
|
a subject resolver is installed and the target’s effective ACL grants the operation’s right to no matching, non-expired ACE |
|
|
|
|
|
a payload whose type the vertex or field cannot take; also |
|
|
an allocation a peer can provoke could not be served from the injected nothrow control seam, or a per-subscriber queue cap is exceeded |
|
|
an |
|
|
a field read or write on a vertex that exposes no such field — an undeclared app field, |
|
|
|
|
|
a transport-construction failure: a dial refused, a TLS/WebTransport handshake rejected, a listener that could not bind, a CAN interface the kernel would not open |
BACKPRESSURE is the allocation-failure and flow-control answer. It is not a
dispatch-depth signal: no depth cap exists.
TRANSPORT_DOWN is the only member of this table whose point is the disposition, not
the name. Its wire code is TRANSIENT in the registry — retry may succeed — while every
other row here is PERMANENT or (for BACKPRESSURE / TIMEOUT) transient for a reason the
caller can already see. Until #929 the built-in transport factories spent NOT_FOUND on a
link that did not come up, so a refused connect went out as tr::path::not_found and a
peer reading the disposition off the code stopped retrying a link that would have come
back. Nothing before #929 could reach err_t::TRANSPORT_DOWN from this side: the map was
total over a status_t that had no member for it.
Setup-time seams¶
Five installers configure a graph before frames flow. Each is set once at wiring time,
from one thread. That is the doctrine, and since #1049 it is stated by the API rather
than requested in a comment: the callback seams are {fn, ctx} slots of one
graph_hooks_t aggregate (RFC-0028 D9) — handed to the constructor, or installed later with
set_hooks (a router wired after its graph reads hooks(), sets its three, and writes the
whole back) — never a std::function.
The shape is the enforcement. A std::function cannot be handed to a racing reader at
all — assigning one destroys the old target, freeing its captures while a reader may be
inside the call — whereas a bare function pointer is one word, so the pair publishes
through a sink_slot_t exactly as the router’s five sinks do. A fan-out,
an ACL gate or a subscribe that races an install therefore sees the whole new pair, the
whole old one, or no sink for that one operation; it never sees a new fn beside a stale
ctx, and never a freed capture. The read costs what the null check it replaces cost: one
relaxed load when nothing is installed.
The two remaining seams are not callbacks and the slot does not reach them, so they take a
lock instead — which is free, because both are control-plane cold. The child-type catalog
is a std::map whose lookup runs from a peer’s bytes; the identity record is a buffer
whose read is served above the READ gate, to a peer that has authenticated nothing
(RFC-0011 §C), and which install and clear both free. Neither lock touches a read, write or
dispatch path.
The ctx pointer is the caller’s, and must outlive every operation that can still reach
the seam: clearing a sink does not stop a dispatch already in flight.
Seam |
Effect |
Default |
|---|---|---|
|
populates the in-band creation catalog: which |
only the built-in |
|
installs the node-scoped record |
absent — |
|
where the producer fan-out hands each remote subscriber’s delivery (the router installs it) |
null — remote subscriber slots are stored but never deliver |
|
maps a caller context to a subject token, enabling ACL evaluation |
none — enforcement is entirely off; every operation is allowed |
|
the inbound |
— (called by the FWD resolver, not a default) |
The two defaults in bold are load-bearing and are the two failure modes a node wired by
hand hits first. A graph with no remote-delivery sink accepts remote subscribes and
records them; nothing ever leaves. A graph with no subject resolver is fully open,
whatever :acl bytes its vertices carry.
set_identity involves no cryptography. The record is a claim: the seam stores and
serves the bytes the owner supplies and verifies nothing. Proving a node holds the key is
authentication and lives elsewhere; a claim is nevertheless what a trust-on-first-use peer
pins and what a topology walk deduplicates by. :identity resolves above the READ
gate, so an unauthenticated peer can fetch it — a narrow, named exemption for that one
field
(RFC-0011 — node identity facet).
A subscriber’s const value_t& is valid for the call only, and it may be an on-stack value_storage_t that a branch write built over a slice it never stored.
Delivery drops¶
A delivery can be lost after the write succeeded. delivery_drops() returns the only
record of it:
struct delivery_drops_t {
std::uint64_t no_target; // no live vertex: an edge's target PATH, or a net-plane route
std::uint64_t denied; // a WRITE was refused by the target's :acl — on any plane
std::uint64_t out_of_memory; // a nothrow delivery clone / edge-view copy could not allocate
std::uint64_t fan_out_truncated; // a wide fan-out's snapshot could not be widened past the
// inline prefix — the capacity degrade, kept apart from OOM
};
The unit is a delivery, not an event: an assign whose pending mark cannot be allocated
sheds every subscriber of the vertex, and a truncated snapshot sheds every edge past the inline
prefix, so each counts once per shed delivery (1 never stands in for N). A HANDLER write
whose notify clone failed used to be the widest case of this; #1505
removed the clone, so that shed is now impossible rather than merely counted.
denied counts a refusal on every plane the value-write path is entered from — an API
write, a FWD{WRITE} terminus, a COMPACT terminus, and a subscription edge’s fan-in gate
— because it is counted at the graph’s own WRITE gate rather than once per deliverer (#1068).
It is therefore refusals, not refusals nobody was told about: an API caller both receives
PERMISSION_DENIED and counts here. A number that depended on which door a refusal came
through could not be summed. assign, a control-plane field write and a denied READ are each
a different right or a different path, and are deliberately not folded in.
A deliverer outside the graph — the net plane resolving a label to a vertex and writing it
— counts its own abandoned deliveries through count_external_drop, the one public door to
these counters. It names only NO_TARGET and OUT_OF_MEMORY: a denial is counted at the gate
that produces it, so offering it there would count one refusal twice.
Counted, never enforced: nothing in the library reads them, so a deployment chooses whether to alarm. They are relaxed monotonic and incremented only on a drop, so the delivering path pays nothing when nothing is dropped. The loads are individually relaxed rather than one atomic snapshot — making them coherent would put a lock on the delivery path to serve a diagnostic, and the useful reading of a monotonic counter is “is this growing”, not an instant.
A subscriber whose target was retired, or whose caller lost the WRITE right, silently stops receiving. There is no other instrument for that.
Declaring owner fields¶
Application properties live under :settings.app. and are declared by the owner, never
invented by a peer. Declaration is a local host call with no wire operation behind it: the
field catalog is device state
(RFC-0010 — owner app fields and schema §A.1).
Every undeclared name answers SCHEMA_NOT_FOUND.
enum class app_access_t { RO, RW, WO }; // constrains REMOTE callers only
struct app_field_t { // owning install
std::string name; // key below settings.app. ("kp", "wifi.ssid")
app_access_t access;
std::vector<std::byte> descriptor; // §B.1 record served verbatim inside :schema
std::vector<std::byte> value; // optional initial value
};
// declared through vertex_policy_t::app_fields (an app_fields_decl_t), at registration or
// through set_policy — one member, two spellings:
g.set_policy(v, {.app_fields = std::vector<app_field_t>{...}}); // owning (moved in)
g.set_policy(v, {.app_fields = kFlashTable}); // borrowed, zero-copy
owning ( |
borrowed ( |
|
|---|---|---|
Name and descriptor bytes |
copied into the graph |
viewed, never copied |
Initial value |
may carry one |
declaration only; write values afterwards |
Caller obligation |
none |
the table array and the bytes it points at outlive the vertex |
borrowed_fields_t converts implicitly from the array spellings a constexpr table in
flash takes, and not from a std::vector — so a caller whose storage cannot satisfy
the lifetime rule fails to compile rather than dangling. A runtime-sized table opts out
explicitly via borrowed_fields_t::unchecked. For an MCU owner whose table is constexpr
in .rodata, the borrowed form costs zero declaration RAM
(ADR-0058 — vertex_ext storage classes, borrowed declarations and group split).
access constrains remote callers only — the owner always reads and writes its own
declared fields. WO gives a secret no read surface, so it never mirrors back.
The runtime validates addressing only: declared or undeclared, and writability. Range
and dtype checking is the owner’s, in handlers_t::on_app_field_write, which fires after a
declared field write has stored its bytes, with the field’s key and the written TLV. That
seam runs outside the vertex lock, so it may re-enter the graph — apply the config,
restructure children, then announce the change with an ordinary data write. An app-field
write never wakes await and never propagates; a change consumers should notice is
followed by the owner’s own announce write.
Pitfalls¶
Rule |
The failure mode |
|---|---|
|
passing a temporary lambda does not compile — which is the intent; a caller that “fixes” it by storing the lambda in a shorter-lived scope than the graph reintroduces the dangle the signature was shaped to prevent |
|
freeing |
Two of the three policies speak for ONE thread’s dispatch domain |
|
A re-entrant unsubscribe needs a parking slot |
|
|
keeping a |
|
calling it on a multi-link rope is not the general path; |
A retired handle stays dereferenceable |
|
|
the seam is read lock-free, so |
No subject resolver means no enforcement |
writing |
No remote-delivery sink means no remote delivery |
remote subscribes are accepted and stored; the |
|
|
|
it neither waits for nor detects readers: it frees parked seams at a moment the embedder names, on the caller’s thread, after releasing the map lock, so a seam destructor may re-enter the graph. Call it only where no lock-free reader holds a seam; it is not the pattern for the subscriber edge array a |
Teardown is only a backstop for parked seams |
the park destructs last, after the map lock and the root, so a seam whose destructor re-enters the graph would re-enter a half-destroyed object. Call |
Consequences¶
Two irreducible operations, not one.
assignandpropagatecompose intowrite; splitting them expresses “update many, notify once” without a batch API, and keeps the coalescing policy on the vertex rather than on the edge.No per-vertex mutex on the value path. The LKV is an atomic pointer swap; the mutex guards the subscriber list, history and
awaitaccounting. Race-freedom under TSan is evidence about data races, not about blocking — the slot’s serializing instructions are real and measured in design/concurrency.Zero-copy fan-out. N subscribers get N refcount clones of one
rope_t, not N copies.No dispatch limits. Delivery terminating at the target removes the cycle, so no depth counter, no hop budget, and no synthetic constant to tune per deployment.
The value is the bytes. A vertex stores a
rope_t, so what it holds is exactly what goes on the wire.
API reference¶
-
struct vertex_policy_t¶
Everything the OWNER declares about one vertex, as ONE aggregate (RFC-0028 §4.12, D12) — what
graph_t::register_vertexand graph_t::set_policy take, in place of the per-vertexset_*wiring verbs slice 10 deleted.Owner-side, host-only, with no wire surface: no peer can read or write any of it, and nothing is inherited (RFC-0022 §3.F). Every member defaults to what an undeclared vertex does, so
vertex_policy_t{}is the default vertex and a caller names only what differs:const auto v = g.register_vertex(path, role_t::STREAM, {}, {.retention = retention_t::N, .depth = 64, .ring_source = &ring_pool, .ring_reliable = true});
A policy is stated WHOLE: graph_t::set_policy applies every member, so a member left at its default resets that property. Applying a member that already holds costs nothing — in particular a default policy on a fresh vertex allocates no extension block.
Public Members
-
std::optional<retention_t> retention = {}¶
What the vertex retains after a write is delivered (RFC-0028 §5.4, D4); unset ⇒ the role’s default (
STORED_VALUE→LAST,STREAM→Nat depth 1,HANDLER→NONE).Legal pairings:
STORED_VALUENONE|LAST,STREAMNONE|N,HANDLERNONE. An illegal one answersSCHEMA_NOT_FOUNDfrom the verb that applies the policy.NONEon aSTORED_VALUEmakes it a pure relay: every write is delivered and released, the write sequence still moves (soawaitwakes),readanswersNOT_FOUND, andassign/propagaterefuse withSCHEMA_NOT_FOUND. A direct write whose only subscribers are callbacks draws no block at all.Nsets the ring depth (depth);NONEon aSTREAMempties and stops the ring.
Switching to
NONEdrops what is already held. Costs zero bytes:NONEis a bit in the vertex’s flag byte, and the depth lives in the extension block a STREAM already has.
-
std::uint32_t depth = 1¶
Ring depth under
retention_t::N(0 behaves as 1); ignored otherwise.
The copy-or-share threshold in bytes (RFC-0028 §5.3, D3): a written value of at least this many bytes is SHARED, one below it is COPIED.
At the terminus, a view-delivered, trailer-less WRITE of at least this size is stored as a refcounted link to the inbound receive segment — no allocation for the bytes, no copy; a smaller one is copied into the value’s own block.
0shares always;SIZE_MAXcopies always. What sharing costs on a POOLED RX backend: the shared value BORROWS a pool slot until it is displaced, so size againstlive shared values x segment_bytes(seeconfig_t::kShareThresholdBytesfor the target-class defaults).
-
mem::block_source_t *ring_source = nullptr¶
The receiving STREAM vertex’s own ring source (RFC-0025 §4.6.1 clause 3); null ⇒ the graph’s graph_t::default_ring_source.
A producer never queues; the queue belongs to whoever consumes it, bounded in BYTES by that party’s own source. Each admitted entry reserves its retained width from the source until it retires. Per-injection-point, never a shared pool: one receiver running its source dry must not affect another. Changing it DRAINS the ring (every reservation goes back to the source that served it). Meaningful only on a
STREAM.
-
bool ring_reliable = false¶
The §4.4 pressure arm for
ring_source:false(default) BEST-EFFORT — a refused admission sheds the oldest entry whole, accounts the loss and raisestr::flow::address_shift_gap;trueRELIABLE — the admission is refused, nothing is shed, and the LOCAL producer’s write answersBACKPRESSURE.
-
delivery_mode_t delivery_mode = delivery_mode_t::IF_NEWER¶
How the vertex participates in an ANCESTOR’s propagate sweep (RFC-0008 §C); default
IF_NEWER. Maintains the sweep’s UNCONDITIONAL membership.
-
app_fields_decl_t app_fields = {}¶
The vertex’s application property field table (RFC-0010 §A) — owning, or BORROWED from static storage (ADR-0058); empty ⇒ no fields (the closed
ENOTTYsurface).Applying a policy REPLACES the table (atomically with respect to concurrent field operations), unless the declaration is the very one already installed — the same borrowed array, or an owning table while one of the same shape stands, in which case stored field values are kept. A borrowed table AND the bytes it points at must outlive the vertex.
-
std::optional<retention_t> retention = {}¶
-
struct graph_hooks_t¶
The graph’s five wiring seams as ONE aggregate (RFC-0028 §4.12, D12) — what
graph_t’s constructor and graph_t::set_hooks take, in place of the fiveconfigure_*verbs slice 10 deleted.Every slot is a graph_hook_t, published through a
tr::sink_slot_t, so a hot-path reader dispatches from one coherent{fn, ctx}snapshot — never a newfnbeside a stalectx. Every slot defaults to null, which is each seam’s documented default, sograph_hooks_t{}is the un-wired graph and a caller names only what it installs:tr::graph::graph_t g{pool, {.subject_resolver = {&resolve, &my_acl}}};
CONFIGURATION, not runtime knobs (#1049): install at wiring time, from one thread, before frames flow. Each
ctxmust outlive every dispatch that can still reach its seam.Public Members
-
graph_hook_t<subject_resolver_fn_t> subject_resolver = {}¶
The ACL enforcement switch (ADR-0018): maps a non-empty caller context to a subject token.
Null (the default) DISABLES enforcement: every operation is allowed and the hot path pays one relaxed load. With a resolver, each gated operation with a NON-EMPTY caller evaluates the target’s effective ACL (own ACEs + inherited, ADR-0020); denial returns
PERMISSION_DENIED. The EMPTY caller context is the local-API convention and is trusted without consulting the resolver (#905), so the resolver’s error arm is free to mean DENY. A token equal totr::graph::kEveryoneSubjectis refused at every gate (#908): the wire has one spelling for a subject token.
-
graph_hook_t<sub_observer_fn_t> subscription_observer = {}¶
The EXTERNAL subscription observer — fired on every
:subscribers[]mutation that arrived over a transport (see sub_event_t for what “external” means).Fires from the one admission door every subscribe lands in and from the
:subscribers[N]clear.evict_link_edgesand a localunsubscribestay silent, by design. Null (the default) costs one relaxed load on the subscribe path.
-
graph_hook_t<remote_delivery_fn_t> remote_delivery = {}¶
The sink the producer fan-out hands each REMOTE subscriber’s delivery to (#136, RFC-0004 §D/§E.1) — the transport plane’s seam;
tr::net::fwd_router_t’s constructor installs it.Fires on whatever thread calls
write(outside the vertex lock), and onsubscribefor a transient-local latch. Null ⇒ remote slots are stored but never deliver.
-
graph_hook_t<wire_target_fn_t> wire_target = {}¶
The wire SUBSCRIBER target resolver (RFC-0021) — the transport plane’s mount descent, borrowed by the
:subscribers[]wire door; the router installs it.Null ⇒ a wire
SUBSCRIBER’sPATHchild is inert, every pre-RFC-0021 embedder’s behaviour. With one, a subscribe whose target routes through a mount binds the edge to(that mount, the residual below it)(#491).
-
graph_hook_t<stats_sampler_fn_t> stats_sampler = {}¶
The NET-PLANE
:statsseam sampler (RFC-0010 Amendment 2, #1503); the router installs it.Null ⇒ the census answers for the graph alone and every
router/labels/linkspelling answersSCHEMA_NOT_FOUND.
-
graph_hook_t<link_hold_fn_t> link_hold = {}¶
The routed-subscription hold seam (RFC-0014 §4, #1816) — the net plane’s standing-binding refcount;
tr::net::transport_vertex_t’s constructor installs it on a build that carries the liveness engine.Null ⇒ subscriptions hold no link, which is every node without that engine. See
link_hold_fn_tfor when it fires.
-
graph_hook_t<subject_resolver_fn_t> subject_resolver = {}¶
-
template<class Fn>
struct graph_hook_t¶ One
{fn, ctx}graph seam: a captureless function pointer and the context handed back as its first argument (ADR-0047, #1049).The shape
receiver_slot_tandhook_talready use. A null fn is the uninstalled seam — the graph’s documented default for each one.
-
class graph_t¶
The L4 in-process graph runtime: the Composite vertex tree plus the whole data API (register / read / write / await / subscribe, ADR-0006).
Vertices form a Composite tree (ADR-0057): each node stores its own NAME segment and its children; a canonical PATH-TLV payload key (docs/reference/02 §dispatch) resolves by an O(segments) child walk at wiring frequency. The hot path resolves a
vertex_t*once — at registration or via one guarded find — then read/write/await on that handle are lock-free in the vertex’s last-known-value slot. Non-copyable; a graph is a fixed runtime root.Public Types
-
enum class external_drop_t : std::uint8_t¶
Why a deliverer OUTSIDE the graph abandoned a delivery before it could write.
Narrow on purpose (#1068). It names only the two ways a net-plane delivery dies without ever reaching write — the route resolves to no vertex, or the payload view cannot be allocated. There is deliberately no
DENIED: a refusal happens AT the graph’s own WRITE gate, which counts it there, so offering it here would let one refusal be counted twice by a caller that also sawPERMISSION_DENIED.Values:
-
enumerator NO_TARGET¶
-
enumerator OUT_OF_MEMORY¶
-
enumerator NO_TARGET¶
-
using child_factory_t = hook_t<result_t<vertex_handle_t>(graph_t&, std::vector<std::byte> child_key, const wire::tlv_node_t *config)>¶
A child-vertex factory: the device-catalog entry ADR-0017 makes concrete.
Given the composed child key (parent key + the SPEC’s
nameNAME) and the optional SPECconfigSETTINGS (a node read in place over the SPEC’s bytes, valid for the call only — #1829), it registers the child vertex(es) and returns the primary handle (or a status — e.g.PATH_IN_USE). The graph owns the addressing (the key is composed for it); the factory owns the catalog (what atypeinstantiates). A hook_t (RFC-0028 D10): itsctxmust outlive the graph.
Public Functions
-
explicit graph_t(mem::block_source_t &src = mem::default_root(), graph_hooks_t hooks = {})¶
Construct a graph drawing every byte it allocates from the single injected
src(#873 phase 1) — the collapsed successor to the four-seam constructor.- What this replaced, and why
Until #873 phase 1 this constructor took four positional, defaulted seams — a
std::pmr::memory_resource*, a mem::mem_backend_t*, and two mem::block_source_t* — so a deployer who wanted a bounded node had to know which of four allocation channels each of its bytes travelled on, and injecting all four still did not bound the process because a fifth channel (the un-injected global heap) ran alongside them. ADR-0079 settled the composition and the 2026-08-26 ruling settled the shape: mem::block_source_t is the substrate, and the graph builds the other two vocabularies on top of the one source it is handed.mem_backend_tsurvives as a wrapper TYPE (mem::source_backend_t), not as an injection seam.- Which bytes flow through after phase 1
ALL of the following, where before they came from four separately-injected places:
the graph’s own tables (#1778): the vertex tree, the vertex index, the link index, subscriber edge tables, the seam park, the creation catalog, the identity record, the declaration lists and the propagate-sweep sets — through the table sub-pool (table_source), in core containers;
the write-path copy-store’s owned value view::segment_t and both folded READs’ exactly-sized POINT headers (ADR-0060, #831) — through an internally-built mem::source_backend_t;
every #551 FAILABLE allocation a peer can provoke: vertex registration, the branch-write decode’s bump upstream, the composed read’s collect stack (control_source);
the graph-level DEFAULT receiver-ring admissions of a STREAM vertex that has declared no source of its own (default_ring_source);
every segment the graph’s READ-BACK encoders mint (#873 phase 3) —
:point,:settings,:settings.app,:acl,:children, the identity record, the stats block, an app field’s stored bytes, and the subscriber / mount-route recordssubscribecomposes. These reachedtr::view::over_bytes’s single-argument, global-heap overload until phase 3 pointed them at value_backend, so a peer’s READ is now charged to the deployer’s slab like every other channel.
- Which bytes do NOT, and where that is tracked
Two carve-outs, both measured or documented rather than pending:
the LKV hazard-slot nodes (
) staylkv_slot.hppnew (std::nothrow)on the global heap. #873 phase 2 built the migration, measured it, and reverted it: +22.7 % on hazard-node acquisition and +3.5 % on the free-list-hit steady arm, with disjoint ranges against an A/A null band of −0.12 %. The seam’s own@notecarries the figures.the read-back encoders’ STAGING buffers, pinned by
tr::wire::emit_tlv’sstd::vector<std::byte>&sink. Phase 3 moved the resulting SEGMENT onto the injection; the transient buffer it is copied from is still the global heap’s. The public signatures that still take or return owningstdtypes are the same kind of residual, and move with the public-API batch of the seam migration.
- Failure convention
The substrate speaks raw
nullptr-on-exhaustion and the graph does not wrap it. The one adapter translates at its own boundary and nowhere else: the backend adapter turns a refusal into a null view::segment_t, which is the BACKPRESSURE signal the write path already answered. The graph’s own tables answer BACKPRESSURE directly. So an injectedsrcbounds the node, and a peer’s CREATE frame can no longer reboot a-fno-exceptionsnode through the failable channels.- NARROW vs WIDE is WHICH source, never a config knob
A host that passes nothing gets mem::default_root (ADR-0083 Decision 4, #1777). Where
kSlabPoolistruethat is the host root (mem_slab_pool.hpp), and the graph DERIVES its sub-pools from it: values (published values, ring admissions and every segment value_backend mints, through mem::heap_backend) from the value sub-pool, and registration and container blocks from the table sub-pool. The platform allocator then sees whole slabs only. Where it isfalsethe default is the platform heap, as before #1777. A bounded node injects a mem::pool_source_t (or a mem::bump_source_t overnull_source()) and the slab’s size IS the bound (ADR-0079): an injected root serves every purpose itself, and no sub-pool is derived from it (derives_sub_pools). Nodefault_config_toption expresses the bound and none will: the divergence is the injected object.- Per-domain overrides still exist, at the seams that own the resource
One injection is the DEFAULT, not a mandate that everything share a store. A STREAM vertex that must not be affected by another receiver’s exhaustion declares its own ring source through vertex_policy_t::ring_source (receiver-pays, RFC-0025 §4.6.1 clause 3) — that seam is untouched, and per-vertex isolation stays a tested property.
Warning
A must not outlive the graph it was read from. This is the one contract the collapse tightens, and it is stated rather than discovered: a stored LKV is a
value_tblock drawn from the graph’s source, so the handle’s lastreleasehands the block back to that source when the last reference drops. Before the collapse that resource was HOST-owned and the host could keep it alive past the graph; now it is a graph member, so a handle released after~graph_tcalls a destroyed object. The graph’s own members are safe by construction — the two adapters are declared FIRST, so they are destroyed LAST, after the vertex tree that holds every stored LKV — but a handle the application copied out is the application’s to drop first. Everyvertex_handle_tobtained from the graph already dangles at that point, so nothing in the reference API is meant to outlive it.- Parameters:
hooks – The graph’s wiring seams (graph_hooks_t), installed before the constructor returns. Default: none — ACL enforcement off, no observer, no transport plane. A router constructed later installs its three through set_hooks.
src – The one nothrow failable block source every allocation above draws from. Host-owned; it MUST outlive the graph and every value handle obtained from it. An injected source must be thread-safe on a target where a value segment’s reclaim can self-route onto a reader/subscriber thread concurrent with a writer’s allocation (ADR-0060 §2) — mem::heap_source is; a mem::pool_source_t must be composed with the target’s arch-selected synchronisation. Any
mem_backend_tis a source too (RFC-0028 slice 10), so a deployer that injects one slab has one slab.
-
inline mem::block_source_t &control_source() const noexcept¶
The injected #551 nothrow failable-block seam (tr::mem::block_source_t).
Exposed so a host can name it in a memory census and so the wiring is observable without reaching into the graph’s state. Callers inside the library draw from
ctl_directly.
-
inline mem::block_source_t &default_ring_source() const noexcept¶
The graph-level DEFAULT receiver-ring source (RFC-0025 §4.6.1 clause 3).
What a STREAM vertex charges its ring admissions against until it declares its own through vertex_policy_t::ring_source, is the value sub-pool (value_source), since #1777. Exposed for the same reason control_source is: so a host can name it in a memory census and so the wiring is observable.
-
inline mem::block_source_t &value_source() const noexcept¶
Where this graph’s VALUES are drawn from (ADR-0083 Decision 3, #1777): every published value, the default ring admissions, and a router’s warm COMPACT copy.
The value sub-pool of the host root (
:stats.mem.values) on a default graph that derives_sub_pools; the injected root otherwise.
-
inline mem::block_source_t &table_source() const noexcept¶
Where this graph’s TABLE blocks are drawn from: vertex registration, the control-plane containers and the failable scratch of a composed read or a branch write (
:stats.mem.tables). On a default graph of akSlabPoolbuild, a table sub-pool of the graph’s OWN, derived from the host root’s platform heap (#1778): independent graphs share no class lock and no cache line. The injected root otherwise, as value_source.
-
void trim_tables() noexcept¶
Return every fully free slab of this graph’s own table sub-pool to the platform heap, on the caller’s schedule (the graph keeps no timer to do it). A no-op on a graph without one (an injected root, or a build without the slab pool); the process-wide sub-pools are trimmed by
tr::mem::host_root().trim().
-
inline mem::block_source_t &net_source() const noexcept¶
The NET sub-pool a router or link on this graph defaults to when the application injects nothing (ADR-0083 Q21,
:stats.mem.net): mem::net_source on a graph that derives_sub_pools, the injected root otherwise.The router and transport sources stay separately injectable (receiver-pays); this is only their default, and the census name a monitor reads it by.
-
inline bool derives_sub_pools() const noexcept¶
Whether this graph derived its sub-pools from the host default root (#1777):
truefor a graph built without a source on akSlabPoolbuild.An injected root serves every purpose itself, so its census is
:stats.mem.controlalone, and:stats.mem.values,.tablesand.netanswerSCHEMA_NOT_FOUND(RFC-0010 Amendment 3: “a node that does not derive a given sub-pool”).
-
inline mem::mem_backend_t &value_backend() const noexcept¶
The tr::mem::mem_backend_t every view::segment_t this graph owns is drawn from (#873 phase 3).
mem::heap_backend for a process-default graph, and the graph’s own mem::source_backend_t over the injected source otherwise — the pointer the constructor resolved once. Exposed for the reason control_source is (census and observable wiring) and because the graph’s own read-back encoders, which are free functions in
graph.cpprather than members, need to name it: every segment the graph mints must come from the one injection, not from the global heap thattr::view::over_bytes’s single-argument overload reaches.
-
vertex_handle_t register_vertex(const path_t &path, role_t role, handlers_t handlers = {}, vertex_policy_t policy = {}, std::span<const payload_right_t> rights = {})¶
Register a vertex at a known-good
pathLITERAL, parsing nothing further (any:fieldtail is ignored) — INFALLIBLE (ADR-0056).The init-time registration form: a
PATH_IN_USEcollision on a compile-site literal is a source bug, not a runtime condition, so this hard-aborts (likepath_t(std::string_view), ADR-0054) rather than yielding aresult_tthe caller would only*-deref unchecked. Returns the pinned vertex_handle_t directly — no*. For a genuine runtime path whose collision is a real outcome, use try_register_vertex.
-
result_t<vertex_handle_t> try_register_vertex(const path_t &path, role_t role, handlers_t handlers = {}, vertex_policy_t policy = {}, std::span<const payload_right_t> rights = {})¶
Register a vertex at
path— FALLIBLE (the runtime-path form of register_vertex).- Parameters:
handlers – The vertex’s user seams (handlers_t); every
ctxmust outlive the registration.policy – The owner’s declarations about the vertex (vertex_policy_t), applied before the handle is returned. A default policy costs nothing. An illegal retention for
roleanswersSCHEMA_NOT_FOUNDand registers nothing.rights – OPTIONAL payload-type → required-ACL-right table (RFC-0014 Amendment 2) — the general contract by which a control vertex demands something other than plain
WRITEfor a given written TLV type. BORROWED for the call: the rows are copied onto the graph, under the same lock that publishes the vertex, so the declaration is in force before the first write can reach it. Empty (the default) ⇒ every write gates onacl_right_t::WRITE, and the vertex carries not one byte for this; the write gate then stops at one relaxed flag-bit test on a word it already holds. A row whosepayload_right_t::typeequals the written value’s leading TLV type supplies the right demanded instead; an unmatched type (and a value whose leading link cannot be read, e.g. a device-memory link) falls back toWRITE. Rows are scanned in order, first match wins. The refusal is still the ONE write gate’s, counted intodelivery_drops_t::denied: this declaration changes WHICH right is demanded, never where the demand is made. (It washandlers_t::payload_rightsuntil RFC-0028 slice 7 took it out of the seam struct.)
- Returns:
The pinned vertex_handle_t, or
PATH_IN_USEif the path is already registered.
-
result_t<vertex_handle_t> register_vertex_key(std::vector<std::byte> key, role_t role, handlers_t handlers = {}, vertex_policy_t policy = {}, std::span<const payload_right_t> rights = {}, std::span<const std::byte> schema_catalog = {})¶
Register a vertex by its canonical PATH-payload
keydirectly (the in-band:children[]path) — FALLIBLE.The key is a composed parent-key +
NAME(child), not parsed from a string. This is the genuine runtime path (a:children[]write can race a duplicate name), so it stays fallible.handlers,policyandrightsare exactly try_register_vertex’s.- Parameters:
schema_catalog – OPTIONAL content of the vertex’s
:schemaSETTINGS— the catalog a CONTROL vertex declares for what its writes accept (RFC-0014 Amendment 3: the creator endpoint’sPOINT{NAME, SETTINGS{…catalog…}}). The bytes are a sequence of encoded TLVs, served VERBATIM and never parsed here; the declaring caller owns their vocabulary and validates writes against it itself. BORROWED for the call: copied onto the graph besiderights, under the same lock and in the same immortal node, so it is in force before the vertex is reachable. Empty (the default) ⇒ the ordinary emptySETTINGS, and a vertex that declares no catalog carries nothing.- Returns:
The pinned vertex_handle_t, or
PATH_IN_USEif the key is already registered.
-
result_t<void> retire(vertex_handle_t vh)¶
Retire a vertex and its whole subtree — the owner-facing mirror of register_vertex (RFC-0009 §A.1 / §B).
Marks
vh(and, per §B.3, every descendant) logically absent: invisible tofind/read/:children[], readingtr::path::not_foundexactly like a never-built path (§C). The allocation is NOT freed and the handle stays dereferenceable forever (ADR-0057 insert-only) — the vertex is emptied, not erased. Retirement re-virginizes each vertex (§B.6): it clears the previous owner’s:acl, value seam, stored value, history, app-field table, subscribers, owner-side storage declarations, and delivery mode, so a later write-creates revive of the same address inherits nothing of the retired owner — in particular the revived path inherits its live ancestor’s ACL policy, never the retired one’s (the §Discussion-7 ruling: an ACL does not survive churn).write_seq_survives (forward-only per address, modulo 2^32).Delivers nothing and wakes no
await(§B.5). Idempotent (§B.4): retiring an already-retired or unregistered vertex succeeds and does nothing. The root cannot be retired. There is no wire operation that reaches here — a peer goes through the device’s own logic (§A.1 / §A.1.1), which is what calls this.
-
std::uint32_t retire_generation(vertex_handle_t vh) const noexcept¶
vh'sretirement generation — the stamp a cached resolution carries (ADR-0062).A
vertex_handle_tnever dangles (the vertex map is pinned and insert-only), but retire re-virginizes the object in place. A holder that caches a resolved handle — a route-handle terminus binding, say — records this alongside it and re-reads it before use: a mismatch means the path was retired (and possibly re-created for a DIFFERENT owner) since the resolution, so the cached answer must be discarded rather than delivered into whatever now occupies that path.Lock-free; the counter is bumped under retirement’s own ordering. Callers must NOT cache an authorization decision this way — a generation match says the vertex is the same one, never that the caller may still act on it (ACL stays per-operation).
-
inline std::uint32_t own_subs(vertex_handle_t vh) const noexcept¶
This vertex’s OWN active subscriber-slot count (#635) — how many slots a delivery here would feed, for sizing and observability.
Relaxed by design: this answers “how much work would a delivery be”, the use vertex_t::own_subs is specified for. A racing subscribe is observed by the next read at worst, which is what a sizing hint needs.
Warning
This is NOT the “is anyone listening” question, on two counts, and a producer must not gate a publish on it — use has_subscribers. It omits subtree subscribers, who subscribe on a strict ANCESTOR and are counted by
listeners_aboverather than here (RFC-0005), so a zero here says nothing about them. And it is the relaxed load, which vertex_t::own_subs_ordered documents as unfit for a skip decision.
-
inline bool has_subscribers(vertex_handle_t vh) const noexcept¶
Would a delivery at
vhreach any subscriber — its own, OR a subtree subscriber on a strict ancestor (RFC-0005)?The gate for a demand-driven producer that wants to skip delivery work. It joins the two gates
deliver_vertex, the per-vertex delivery unit, applies —fan_out’s own self-gate on the own count, then thelisteners_abovegatedeliver_vertexholds overbubble_up— so a producer that skips adeliver_vertexonfalseskips exactly what that call would have found no receiver for. (A decomposing BRANCH write is not onedeliver_vertex: it fans out at each descendant landing site under that site’s own gate, which this predicate does not answer for.) Gating on the own-slot count alone silently drops every subtree subscriber, which is why own_subs carries a warning against it. (mark_pending, the deferred half, gates ondelivery_modefirst and so asks a third question this predicate deliberately does not.)Note
Swapping the
seq_cstown half for the relaxed vertex_t::own_subs leaves the whole suite green, so its presence here rests on that argument, not coverage.Warning
Subscribers are not the only consumers.
readpollers and threads blocked in await are invisible here — this counts subscription edges only, which ADR-0006 makes a field-write to:subscribers[]rather than one of its three verbs. A producer that skips its delivery onfalseis fine; one that also skips the VALUE STORE starves every awaiter (nowrite_seq_bump to wake them) and freezes the LKV for every reader.Warning
A skip here has no durability-latch backstop. ADR-0049’s latch belongs to vertex_t::own_subs_ordered’s fan-out skip, whose protocol is store the LKV, THEN load the count; a producer that skips on this predicate never reaches the store, so there is no new value to latch. What ordering this predicate does give is just its two loads’ — the
seq_cstvertex_t::own_subs_ordered and the relaxedlisteners_above— making it exactly as ordered asdeliver_vertex’s own two gates and no more. Theseq_csthalf’s argument is documented there; it is not restated here.Warning
The ancestor half can be one subscribe behind, with no bound but the platform’s.
listeners_aboveis a relaxed load, so afalsehere may miss a subtree subscribe that has already COMPLETED on another thread — latch taken, counter bumped — and nothing synchronizes when this reader catches up. That staleness is deliberate and ruled on measurement (#854, REFUTED — theseq_cstcandidate doubled the idle write’s fence count on rv32 and bought nothing): per the #555 standard, the outcome a stale-falseskip produces — the racing publish reaching no subtree subscriber — is indistinguishable from the write linearizing BEFORE the subscribe, and the subscriber’s ADR-0049 latch cannot contradict that ordering, because the latch snapshots the SUBSCRIBED ancestor’s own LKV (vertex_t::add_edge), which never holds a descendant’s value. There is no forbidden observation for an ordered load to exclude, so the ordered load does not exist.
-
std::size_t vertex_slot_count() const noexcept¶
Slots in the node-scoped vertex index — the cardinality a bound-path element’s index is bounds-checked against (RFC-0024 §6.4).
One slot per
vertex_tever allocated in this graph, in allocation order, slot 0 being the structural root. The index is append-only because registration already is (“vertices are added, never erased”), so a slot handed out once names the same allocation for the graph’s lifetime and there is no new invalidation event to observe.Node-local and unobservable on the wire: a peer learns another node’s cardinality only by being handed an element that came from it, and an element is meaningless anywhere but on the host that minted it.
-
void set_vertex_ceiling(std::size_t max_vertices) noexcept¶
Cap the node’s vertex population at
max_verticesallocations, charged against the vertex_slot_count census (#1314).The census already counts every
vertex_tthis graph ever allocated — including the placeholders a descent materializes and the landing sites an RFC-0005 §D branch write decomposes into. What it did not do was charge anything: every creation door (registration, the write-createmkdir -p, branch-write decomposition) allocated until the allocator itself refused. A branch writer that is already resolved and already WRITE-gated is therefore governed — every landing site passes its CREATE/WRITE gate — but its landing sites cost nothing, so “more writes, wider writes” is an unbounded vertex population multiplied by a peer’s choice. That is the peer/writer-multiplied allocation class ADR-0079 fences elsewhere: a per-call bound a caller can multiply is not a node bound.This is the node bound, and it is the #838 shape — count, then act — over the census that already exists rather than a second, bespoke counter. Past the ceiling every creation door answers
BACKPRESSURE, the same exhaustion status an injected tr::mem::block_source_t answers with, so a caller that already handles a refusing store needs no new vocabulary. Refusals are counted (vertex_ceiling_refusals) so a node can see the bound bite instead of inferring it from a failed write.Policy stays with the deployer, per ADR-0079 §Decision 4: the default is kNoVertexCeiling, so an un-sized node behaves exactly as before and the library fixes no synthetic limit. When ADR-0079’s stage-2 graph placement store lands and
vertex_titself draws from the injectedctlseam, the store’s size becomes the natural bound and this ceiling becomes the coarse-grained backstop rather than the primary one.Note
The census is append-only (retirement revives in place, it does not free), so the ceiling is a high-water mark on ALLOCATIONS, not a live occupancy that a retire gives back. That matches what it is bounding — memory a peer made this node commit — and it is why no release path is needed.
Note
A refusal mid-descent leaves the levels already created in place, exactly like an ACL denial partway down a write-create chain (“created-but-empty intermediates may
persist past a later denial”, RFC-0005 §ACL). The bound holds regardless: those levels are themselves charged.
Note
Session identity anchors are NOT charged here. They take a census slot but are created through register_session_anchor, which is already bounded by the listener’s
max_peersaccept policy; charging them twice would let graph growth refuse a session admission the accept policy had already granted.
-
std::size_t vertex_ceiling() const noexcept¶
The ceiling in force (kNoVertexCeiling when unset).
-
std::uint64_t vertex_ceiling_refusals() const noexcept¶
How many creations the ceiling has refused since construction (monotonic).
-
result_t<vertex_handle_t> register_session_anchor(std::string_view id)¶
Register — or REVIVE — a session identity anchor: a vertex that exists to be REFERENCED and never to be ADDRESSED (#1223 step 2).
ADR-0044’s 2026-08-13 amendment scopes §Decision 1 to announce-census peers and lets an accepted ws/tcp session hold a vertex, so that the session’s death is a RETIRE and a route naming it fails the RFC-0024 §5.1 generation check. This is the seam that gives it one.
idis the session’s node-scoped identity string — the router composes it from the mount’s qualified name and the peer’s slot name, so the SAME slot always asks for the SAME anchor.An anchor is not part of the addressable tree, deliberately. It hangs off a private structural root that
roots_cannot reach, so:find,read, every path descent and every:children[]listing are byte-for-byte unchanged — an anchor is invisible to all of them. That is what keepsbus_link_t::enumerate_peersthe ONE source of truth for a bus vertex’s synthesized members (ADR-0044 §Decision 1, unamended in this respect), instead of a second one.nothing below a bus mount becomes locally resolvable, so RFC-0020 §3’s MUST (“a node
MUST NOT resolve the residual against its local graph”) keeps the premise it was argued on. An anchor cannot be the shadow vertex that MUST is about, because no spelling of any
dstreaches it. What an anchor DOES have is the only thing it is for: a slot in the pinned, insert-only vertex map, hence a(index, generation)an RFC-0024 element can name.
Revive is in place. The anchor for a given
idis allocated ONCE and re-filled afterwards, exactly as a retired addressable vertex is revived by a second registration at its path — so a recycledp<slot>returns the SAMEvertex_tin the SAME slot with only the saturating retire generation bumped (RFC-0024 §4.4 rule 3). Anchor count is therefore bounded by the listener’smax_peers, not by session churn, which is the measurement the ADR amendment rests on.Retire an anchor through the ordinary retire — it is an ordinary vertex in every respect the mint, the deref and retirement care about.
- Return values:
status_t::PATH_IN_USE –
idalready names a LIVE anchor (a duplicate arrival notification, or an id collision). The caller keeps the existing anchor.
-
std::optional<vertex_handle_t> find_session_anchor(std::string_view id) const¶
The live anchor for
id, orstd::nulloptwhen none is registered (it was never created, or it has been retired). Never descends the addressable tree.
-
std::size_t session_anchor_slots() const noexcept¶
How many anchor
vertex_ts this graph has ever ALLOCATED — live or retired.The bounded-across-churn number, exposed so a test can assert it rather than infer it: it counts allocations, not registrations, so a revive must leave it unchanged.
-
std::optional<vertex_slot_t> vertex_slot(vertex_handle_t vh) const noexcept¶
This node’s own reference to
vh— the MINT side of a bound-path element (RFC-0024 §6.4, §7).Returns the index and the generation that stamps it, because the two are one fact: read separately they can straddle a
retire, and the pair would then name the successor tenant’s vertex while the caller believes it bound the one its operation reached. Both fields are read under a singlemap_mutex_hold, which retirement takes uniquely, so the pair is always a consistent snapshot.The memo was previously declined on a footprint argument — “4 bytes on rv32,
where `sizeof(vertex_t)` sits at its ratchet with zero headroom” — and the #1487 census falsified its premise. There are 4 dead bytes at object offset 36–39 on BOTH ABIs, and spending them costs zero:
sizeof(vertex_t)stays 96 / 72 and bothconfig_tratchets still pass, pinned to their measurements. The price is a layering compromise, not RAM — the bytes are only reachable from insidepath_key_t(they arename_’s tail padding on x86-64), so the memo lives there, documented as borrowed. A pointer→index side map, the other candidate, would still cost strictly more than the 4 B/vertex RFC-0024 §6.4 priced.What made it worth spending is that the scan was O(N) under the shared — 450 ns at 10³ resident vertices and 410 µs at 10⁶ (#1485/#1496) — so route formation over M bindings paid O(M×N) and every concurrent reader queued behind the hold. The mint is once per binding, but “once per binding” is M times, not once.
Note
This is a control-plane call, but it is no longer priced as a scan (#1486). The reverse direction IS memoized: every vertex carries the index of its own slot, stamped once at slot assignment under the same unique hold that appended it, and this call validates that memo against the index and returns. The scan survives only as the fallback for a
vertex_tno graph slotted.Warning
The memo is INTERNAL. It is not exposed on
path_key_t, is not exposed here, and is not a second staleness signal: the generation remains the whole of that (RFC-0024 §5.1). The hot path — deref_vertex_slot — pays a bounds check and one compare and never comes here.- Return values:
std::nullopt –
vh'sgeneration has SATURATED (kGenerationSaturated), so the vertex is permanently unbindable and the caller stays on the canonical form (RFC-0024 §4.4 rule 3) — or, defensively,vhis not in this graph’s index at all.
-
std::optional<vertex_handle_t> deref_vertex_slot(std::uint32_t index, std::uint32_t generation) const noexcept¶
Dereference a bound-path element — the §5.1 check, and the whole of it.
Bounds-checks
indexagainst vertex_slot_count, refuses a SATURATEDgenerationoutright, and compares the rest against the slot’s retire_generation. The vertex map is pinned, pointer-stable and insert-only, so an in-range index always names a live allocation and the deref itself cannot fault.A generation only ever moves forward, so a stale element can only ever compare lower and never becomes valid again by waiting — except at the ceiling, where the counter stops. There, and only there, “moves forward” stops being a guard: a
kGenerationSaturatedelement would match the slot for the rest of the node’s life, across every subsequent retire and revive, so staleness detection would be dead for that slot and the #603 misroute class the saturation rule exists to close would be open again. The mint refuses to issue such an element; this refuses to honour one, which is what makes “permanently unbindable” (RFC-0024 §4.4 rule 3) a property of the vertex rather than of one code path’s good manners.Warning
A match authorizes nothing. It says the vertex is the same one, never that the caller may still act on it: every bound-form operation re-evaluates
acl_allowsat the dereferenced vertex for its own right, exactly as the canonical form does (RFC-0024 §6.2). The graph’s own data ops do that themselves, which is why the two spellings are equivalent by construction.- Return values:
std::nullopt – Out of range, saturated, or the generation does not match. The caller MUST then drop — never forward, never apply, never repair (RFC-0024 §5.3).
-
std::optional<vertex_slot_t> vertex_slot_at(std::uint32_t index) const noexcept¶
The element a mint would issue for the slot at
index— the FORWARDER’s mint (RFC-0024 §7.1 step 2), in O(1).The terminus mints for a vertex it just resolved, so it has a handle and can afford vertex_slot’s scan. A forwarder mints for the connection vertex of the link a reply arrived on — a vertex whose index it recorded once, at registration — so all it needs is that index’s CURRENT generation, and paying a scan of the whole index per forwarded reply to re-derive an index it already holds would be the wrong shape at the wrong place. This is the same read the other way round: index in, generation out.
- Return values:
std::nullopt –
indexis out of range, the slot’s generation has SATURATED — a permanently unbindable vertex (RFC-0024 §4.4 rule 3) — or the slot holds a retired/never-registered PLACEHOLDER, whichderef_vertex_slotrefuses on the honouring side and which is therefore refused here too: otherwise the window between a retire and its revival mints an element valid against the SUCCESSOR tenancy. A forwarder that cannot mint STRIPS the mint answer (§7.1 erratum 1) and the origin stays canonical.
-
bool allows(vertex_handle_t v, std::string_view caller, acl_right_t right) const¶
Evaluate the ACL at
vforcallerandright— the §6.2 check, exposed.The same predicate every data op already runs before it acts, published for the ONE caller that reaches a vertex without performing a data op on it: the bound-path forwarder, whose element dereferences to a connection
vertex it will egress through rather than read or write (RFC-0024 §6.2 — “every operation arriving on a bound
path MUST evaluate <tt>acl_allows</tt> at the dereferenced vertex, for the operation’s own
right”). Nothing is cached: an
:aclwrite marks the subtree dirty and the next call rebuilds, so a revoked right takes effect on the very next frame over an already-minted binding.- Parameters:
v – The vertex to evaluate at.
caller – The subject context — a transport link name; empty is the trusted local caller, which is allowed everything (the shipped convention).
right – The right the operation needs.
-
void collect()¶
Free every value seam retire parked — the EXPLICIT collector (#576).
retire detaches a vertex’s value seam and parks it: the seam is read lock-free, so the retiring thread cannot free the block a concurrent reader may still be dereferencing. Parking alone has no other end, so a node that retires seam-bearing vertices repeatedly grows the park forever. This is that other end, and it is the embedder’s call, not the library’s.
Which vertices park — handler PRESENCE, never role.
vertex_t::adopt_identityallocates thevalue_handlers_tiff at least one ofon_read,on_write,on_childrenwas installed at registration;role_tis never consulted. So arole_t::STORED_VALUEvertex registered with anon_childrenparks one seam on retirement, and arole_t::HANDLERregistered with an empty handlers_t parks nothing. Scoping a quiescent point by role excludes exactly the production case below.The one peer-driven append site is conditional.
tr::net::transport_vertex_t::remove_connectionretires the/net/<module>/<name>identity vertex, which is registeredrole_t::STORED_VALUE— and it bears a seam only when its link exposes a bus facet (transport_t::bus() != nullptr): the CAN binding, and a tcp/ws server wiredpeer_named = true, get anon_childrenthat synthesizes the live peer listing (ADR-0044). A point-to-point deployment — every dial link, UDP, loopback, a default-wired server — parks nothing on teardown and needs no quiescent point at all. A bus node parks onevalue_handlers_t(~96 B ofstd::function, plus each callback’s captures) per teardown; that node is the one this method exists for.The free runs on the CALLER’s thread and OUTSIDE every graph lock: the parked list is swapped into a local under the map lock, and the local destructs after the lock is released. So a seam callback’s destructor may re-enter the graph (drop a handle,
finda path, retire something else) without deadlocking, and an arbitrarily slow destructor blocks no reader or writer.Idempotent, and a no-op when nothing is parked. Not itself a reader-safety mechanism: it neither waits for nor detects readers. An embedder that never calls it keeps the pre-#576 behaviour — the park grows without bound — which parked_seam_count makes observable.
Warning
The caller MUST call this from a point where no lock-free reader holds a value seam. The library cannot know that moment — a reader holds the raw seam pointer across the user callback it invokes — so naming it is an API obligation this method hands to the embedder. On a single-threaded node any point between operations qualifies. On a threaded node, a point where the graph is quiescent for reads does: after the transport plane’s receive threads are joined or paused, or on the one thread that runs every graph operation. The hazard is NOT limited to a thread that started on an already-retired vertex:
read/write/:children[]load the seam pointer ONCE (deliberately — a second load could see a concurrent retire’s null), so a thread that entered while the vertex was still LIVE holds that raw pointer across the whole user callback, and a retire landing mid-callback moves the block it is using into the park. Collecting while any such call is in flight — retired first or not — is a use-after-free.Note
Whatever is still parked when the graph is destroyed is freed by the graph’s own teardown — a backstop against unbounded growth, NOT a substitute for this call.
retired_seams_is declared beforemap_mutex_androots_, so it destructs last, after the vertex tree and the map lock are already gone: a seam callback whose destructor re-enters the graph re-enters a half-destroyed object and crashes. Such an owner is safe HERE and only here — it must be collected explicitly, never left to teardown.
-
std::size_t parked_seam_count() const¶
How many retired value seams are currently parked, awaiting collect.
The observability half of the collector: an embedder that never calls collect has a number it can watch (a health field, an assert in a soak test) instead of a silent, peer-driven leak. Grows by one per retired vertex that BORE a value seam — i.e. one that had any of
on_read/on_write/on_childreninstalled at registration, whatever itsrole_t— and drops to zero on collect. A retired vertex with no value seam parks nothing, including arole_t::HANDLERone registered with an empty handlers_t. On the transport plane that means one per/net/<module>/<name>identity vertex whose link exposes a bus facet (CAN, or a tcp/ws server wiredpeer_named = true) and zero for every point-to-point connection — so on a default deployment this legitimately never leaves 0.
-
std::size_t evict_link_edges(std::string_view link_name)¶
Evict every subscriber edge a departed link left behind — the graph half of link-teardown eviction (RFC-0009 §D, extended to peer departure).
Walks the whole graph and deactivates + RECLAIMS each active subscriber edge whose stored link NAME equals
link_name(the NAME this node addressed the link by — a bus peer’s tag, or a point-to-point child’s registered NAME), unwinding the RFC-0005 listener bookkeeping for each. Local edges and edges of other links are untouched; slot indices of surviving edges never renumber (§D.2), and the freed slots are reused by later appends (vertex_t’s add_edge reuse) — so a redialing peer’s re-subscriptions reoccupy the memory its dead session held instead of growing every vertex’s edge list forever.A local, host-facing API in the §A.1 sense: no wire operation reaches here — the transport plane calls it when it LEARNS a link died (
fwd_router_t:: link_down, the link-departure hook), exactly as the owner’s own logic might. Concurrency: the vertex set is snapshotted under a sharedmap_mutex_hold, then each vertex is evicted under its own stripe lock inside a fresh shared hold (never across vertices), so concurrent writes/deliveries interleave freely; an in-flight delivery keeps its route alive by refcount clone (ADR-0041 §2). Safe to call for a link that never subscribed (a no-op).An EMPTY
link_namematches nothing and returns 0. This entry point reports a COUNT and has no error channel, so a nameless link is a no-op rather than a status: a link with no name never subscribed anything. It is a rule, not a coincidence of the comparison — a LOCAL admission stores the empty caller context, so before #1056 an empty key compared equal to every local edge that carried a cold half (thedelivery_compactopt-in) and reclaimed it graph-wide.- Parameters:
link_name – This node’s NAME for the departed link; empty ⇒ no-op, 0.
- Returns:
The number of edges evicted, summed over the graph.
-
std::size_t link_edge_candidates(std::string_view link_name) const¶
How many vertices a evict_link_edges for
link_namewould EXAMINE — the departure’s cost, observable (#1071).A diagnostic, and the instrument the #1071 acceptance test asserts on: before the per-link index this number was “every vertex in the graph holding any subscriber
edge”, so one peer’s hangup was priced by every OTHER peer’s subscriptions. It is now the count of vertices that peer itself ever subscribed on.
Reports the INDEX’s size, not a live edge count, and the two differ by design: the index is a superset that keeps a vertex after an individual unsubscribe (see
link_index_t), so this can exceed the number of edges an eviction would actually reclaim. It is an upper bound on work, which is exactly what the scaling property is about — never read it as “how many edges this link has”.- Parameters:
link_name – This node’s NAME for the link; empty ⇒ 0 (a local edge is not reachable by link teardown).
- Returns:
The number of candidate vertices, i.e. the departure’s bounded cost.
-
link_id_t intern_link(std::string_view link_name)¶
Mint-or-find
link_name'sinterned token — the LINK-UP door (#1266 / #1417).The transport plane calls this ONCE, when a link or a bus peer becomes audible, caches the answer in its own per-link receive context, and hands it to every subsequent subscribe through
op_resolver_t::on_link_id. That is the whole of the carry, and it is what takes the index operation from 14.1–17.2 ns to 9.4 ns — FLAT in link count — and its footprint from 175.8 to 128.0 bytes per link (measured on #1416’s harness,bench/bench_subscribe_index, 8 vertices, best-of-13-rounds against a carried A/A null).IDEMPOTENT BY NAME, which is the property #1263 pinned and this must not move: the same spelling always answers the same live token, so a redialing peer that comes back under its old name re-enters its old slot rather than stranding it. A name that has never been interned takes a fresh slot (or a released one), and the returned token is valid until release_link or a whole-link eviction retires that slot.
- Parameters:
link_name – This node’s NAME for the link; empty ⇒ an invalid token (the #1056 empty-key rule — the LOCAL spelling is not a link).
- Returns:
The token, or a default-constructed one for an empty name.
-
link_id_t intern_link_hinted(std::string_view link_name, std::uint32_t &hint)¶
intern_link with a caller-held SLOT HINT — the O(1) door for a caller that has somewhere stable to keep one word (#1437).
intern_link’s mint-or-find goes through the name door, and that door is a LINEAR SCAN of the live slots (
link_index_t’s “DENSE SLOT VECTOR” block spells out why — deliberately NOT an@ref, because that anchor lives on an implementation type the rendered API reference does not emit, and a link to it is a-n -Wsphinx failure). Cheaper than the hash it replaced up to about 32 live links and about 2x dearer at 65 — which is fine on the paths the trade was argued on (once per peer hangup) and NOT fine on the un-carried subscribe doors, which pay it per subscribe. This is those doors’ way out: a caller that owns a durable word per link (a registered mount, say) keeps the slot number in it and gets the whole find for one bounds check and one name compare.The hint is a PURE CACHE with NO invalidation contract, which is the property that makes it safe to park in a structure nobody synchronizes: a stale hint — a slot released and re-minted under another name, a hint from a different graph, an uninitialized zero — fails the name compare and costs one wasted comparison before the scan it would have paid anyway. Nothing a wrong hint can spell produces a wrong token, so the caller owes this word no upkeep on link-down, on re-add, or on teardown.
The generation is NOT part of the hint and must not become part of it. The slot’s name IS the validation: a live slot’s spelling is unique across the index (intern_link is mint-or-find by name) and a dead slot spells itself empty, so a name match already proves the slot is live, is this link’s, and carries the stamp that is current right now — read from the slot under the same lock rather than remembered by the caller.
- Parameters:
link_name – This node’s NAME for the link; empty ⇒ an invalid token, hint untouched (the #1056 empty-key rule).
hint – In: where this name was last seen. Out: where it is now, on any non-empty name that interned — so the next call is the fast path.
- Returns:
Exactly what intern_link would answer for
link_name.
-
void release_link(link_id_t token)¶
Retire
token'sslot so it can be reused — the LINK-DOWN half of intern_link.Bumps the slot’s stamp, so every copy of
tokenstill in flight stops validating and degrades to a name lookup rather than addressing whatever link takes the slot next. The candidate list is released with it: this is the teardown door, so anything still listed is by definition an edge that departed with the link.Optional in the safety sense and NOT in the footprint sense: a node that never calls it keeps one 64-byte slot per distinct link name it has ever seen, where one that does keeps one per link it currently holds.
evict_link_edgesalready releases the slot it empties, so the transport plane’s ordinary hangup path needs no extra call; this exists for an owner tearing a link down without evicting through the graph.A token that is invalid, out of range, or already released is a no-op.
-
std::size_t link_index_name_lookups() const¶
How many index inserts had to fall back to a NAME LOOKUP — the carry’s own observability (#1417).
A carried token that validates costs a subscript; anything else costs a scan of the live slots. This counts the second case, and it is the ONLY way to see from outside whether the carry is actually working: a valid token and a name lookup produce byte-identical index state by construction, which is what makes the carry safe and also what makes it invisible.
Expected to be SMALL and bounded, not zero. It counts one per link that has never been interned (the transport plane’s lazy mint goes through
intern_link, so that is not counted), one per admission whose key is not the arrival link — a mount-routed target, afield_writecallerfallback — and one persubscribe_wirereached with no transport plane behind it, which is every host-side caller and every test that binds an edge directly. The purely LOCALsubscribe()doors never reach here at all: they admit noremote, so there is no link to index them under. A count that GROWS with traffic on a steady link set means the carry is not reaching the index, which is a performance defect, never a correctness one.
-
std::size_t evict_route_edges(std::string_view link_name, std::span<const std::byte> route_wire)¶
Evict the remote subscriber edge(s) whose delivery link AND stored return route both match — the refused-route reclaim (#1223 step 5).
The narrow sibling of evict_link_edges, fired by the transport plane when a delivery it emitted draws back an addressed
tr::path::invalidrefusal (the RFC-0020 bus-residual reject — the one wire observation a producer gets that a stored route’s terminal session departed). Where link teardown reclaims a whole link’s edges, this reclaims exactly the edge(s) that delivered alongroute_wireoverlink_name:both keys are required, and the route compare is BYTE-equal on the stored PATH TLV (seevertex_t::evict_route_edges). Same two-phase locking, same RFC-0005 unwind, same no-error-channel contract as link teardown; an empty key matches nothing.RFC-0009 §D.4 is NOT contradicted: that clause keeps an edge whose target vertex
retired, on the stated premise that “a write to a retired path is not an error the
producer observes”. A refused ROUTE is precisely the case where the producer now DOES observe an error — RFC-0020 (which postdates §D.4) made the observation normative, and this reclaim acts only on it.
- Parameters:
link_name – This node’s NAME for the link the refusal arrived on.
route_wire – The refused route — whole TLV bytes echoed by the rejecting hop: a canonical PATH, or (RFC-0024 §7.1 amendment 1) the bound
PATH_REFa reverse-list delivery was refused as. This door classifies the type byte; the per-vertex half stays wire-type-agnostic.
- Returns:
The number of edges evicted, summed over the graph.
-
std::optional<session_anchor_route_t> session_anchor_route(vertex_handle_t vh) const noexcept¶
Classify
vhper the block above: the anchor’s (mount, peer), or nullopt for every ordinary vertex. Lock-free — the key record is immutable.
-
template<typename Fn>
inline bool for_each_vertex(Fn &&fn) const¶ Visit every REGISTERED vertex once, in ascending canonical-key BYTE order — the graph’s enumeration surface (a census, a directory listing, a paginated
/system/…projection).fnis invoked asfn(wire::key_view_t key, vertex_handle_t vh).keyis the vertex’s full canonical key (concatenated NAME records, thePATHpayload) rendered on demand — ADR-0057 stores one segment per node, so no such key exists until this asks for it — and is BORROWED for the duration of that one call. Placeholders (the unregistered intermediates a deepregister_vertexcreates) are SKIPPED: they are addressing scaffolding, not vertices an owner declared, andfinddoes not answer for them either.SORTED, and there is no unsorted twin, deliberately. The tree walk’s natural order is
for_each_descendant’s, which no caller should encode a dependency on; a consumer paginating this surface — “give me vertices 40..60” across two operations — needs the order to be the SAME both times whenever the graph did not change, and byte order over canonical keys is the only order this container can promise that of. The sort is not what costs: every visit needskey, and rendering the keys is alreadyO(n)allocations, so ordering them is a comparison pass on top of work the unsorted form would have done anyway. Offering both would buy nothing and invite the wrong one.The order is the SAME one the RFC-0008 sweep sets are kept in (
pending_/unconditional_, byte-keyed sorted tables), and it earns its keep the same way: the length-prefixed NAME framing makes a parent’s key a byte-prefix of every descendant’s, so a parent always precedes its subtree and that subtree is a CONTIGUOUS run. The result therefore reads as a stable pre-order tree listing.Concurrency: the {key, vertex} snapshot is taken under ONE shared
map_mutex_hold andfnruns OUTSIDE it — the same two-phase discipline evict_link_edges and the fan-out sweep use. SofnMAY re-enter the graph (read a value, register a vertex, retire something) without self-deadlocking. What it gets in exchange is a SNAPSHOT: a vertex registered after the hold is not visited, one retired during the walk is still visited (handles stay valid — vertices are pointer-stable and never freed, ADR-0057), and a caller that must distinguish those re-reads under its own lock. Vertices registered BEFORE the hold are all visited.Note
It is byte order over the KEY, not alphabetical order over the spelled path. A NAME record is
02 00 <u16 len> <text>, so siblings sort by name LENGTH first and only then by text (/zonebefore/sensorbefore/actuator). A consumer that wants alphabetical DISPLAY order sorts what it collected; what this promises is stability and subtree contiguity.Warning
CONTROL-PLANE ONLY and priced as such: it allocates one owned key per registered vertex plus the snapshot array, all from the graph’s table source, then sorts. Do not put this on a delivery or write path.
- Returns:
false when the table source refused the snapshot (#1778):
fnwas called for nothing. A walk either visits every vertex or none.
-
void register_child_type(std::string_view type, child_factory_t factory)¶
Populate the device creation catalog (ADR-0017): map a SPEC
typeselector to a child_factory_t.A
:children[]SPEC write whosetypeis unregistered returnsSCHEMA_NOT_FOUND(the ENOTTY of an unsupported creation). The built-instored_valuetype is registered by the constructor.CONFIGURATION, like the
graph_hooks_tseams: populate the catalog at setup, before frames flow. Unlike them the catalog is a sorted table, so #1049’s{fn, ctx}publication does not reach it — a concurrent insert moves entries the in-band creation path may be reading. Registration and lookup therefore take a lock, which costs nothing: both are control-plane cold (one lookup per created vertex) and neither is on a read, write or dispatch path. Violating the setup-only contract is consequently slow rather than corrupting.The entry draws from the graph’s table source (#1778). Setup is where the catalog is sized, so a refusal here is a sizing bug: it aborts with a message naming the source and the bytes it was asked for (ADR-0056, ADR-0083).
-
result_t<value_ref_t> read(vertex_handle_t v, std::string_view caller = {}) const¶
Read a resolved vertex’s stored value (the hot path — lock-free in the LKV slot).
Returns the last-known-value as a value_ref_t (RFC-0028 D11, one read type): a reference to the published block, not a copy. A scalar is the single-link case; a consumer needing contiguous bytes calls
value_t::only()(single-link, zero copy) orvalue_t::materialize(), and one that needs arope_tclones it withvalue_t::rope(). The trailingcalleris the ACL caller context (#81): empty for a local API call (the default — zero churn), the inbound link NAME when the FWD resolver drives the op. With no subject resolver installed it costs one null check.A vertex with ≥ 1 registered child serves the COMPOSED BRANCH READ instead — the folded POINT tree of read_subtree_folded (per-node stored TLVs verbatim, READ-denied subtrees pruned): a view over the existing last-known-value ropes, not a copy. Leaf reads are byte-identical to the pre-composed-read behavior, and a HANDLER target’s
on_readseam keeps precedence over the composed read.
-
result_t<void> write(vertex_handle_t v, view::rope_t value, std::string_view caller = {}, const net::link_kind_t *link = nullptr)¶
Write a resolved vertex’s value:
assignthen deliver (RFC-0008 §D).Takes a rope; an existing
view_tcaller compiles unchanged via the implicitview_t→rope_t.calleris the ACL caller context (see read).linkis the transport-catalog(kind, role)of the link the write arrived on, or null for a write that arrived over none (#1650). It gates nothing here: it reaches the vertex’s admission filter and handler aswrite_ctx_t::link, besidecalleraswrite_ctx_t::subject. The router passes it for a write a link carried; an API write leaves it null.
-
result_t<void> write(vertex_handle_t v, const field_path_t &field, view::rope_t value, std::string_view caller = {}, const net::link_kind_t *link = nullptr)¶
Field-write by handle: resolve the vertex_handle_t and field_path_t once, then reuse them on the hot path — no string parse, no map lookup per call.
An empty
fieldis an ordinary value write. Passpath.field()for the field selector. A field write targets a contiguous control TLV, so a multi-link value is materialized first.linkis as for the plain overload: an emptyfieldhands it tohandlers_t::on_admitandon_write, and a:settings.app.<name>field write hands it, withcaller, tohandlers_t::on_app_field_admitin the samewrite_ctx_t(#1832).
-
result_t<void> assign(vertex_handle_t v, view::rope_t value, std::string_view caller = {})¶
Assign a vertex’s value — the STATE transition only, sends NOTHING (RFC-0008).
One of the two irreducible operations
writecomposes: swap v’s last-known-value (atomic), append to the stream ring, bump the write sequence (waking await), and mark v for the next covering propagate sweep (unless v is EXPLICIT, or nobody observes at/above it). WRITE-gated like write; never gated by delivery_mode. A branch POINT decomposes and assigns each descendant (no notify). Pair with propagate for the “update many, propagate once” workflow.- Return values:
SCHEMA_NOT_FOUND –
v'srole RETAINS NOTHING (aHANDLER), so the state half has nowhere to land and the covering sweep — which takes no value argument and reads the last-known-value — would deliver silence (RFC-0008 Amendment 2). Checked after the WRITE gate. Use write instead: it dispatches theon_writeseam and delivers eagerly, which is what a non-retaining vertex can actually do.
-
result_t<void> propagate(vertex_handle_t v)¶
Propagate along subscription edges — the EDGE transition only (RFC-0008 §B/§C).
Delivers v’s current value (always —
vis the explicit target, so a direct propagate is never gated by v’s delivery_mode) AND the qualifying descendants of v’s subtree per each descendant’s delivery_mode: IF_NEWER descendants assigned since the last covering sweep, and every UNCONDITIONAL descendant. Reads the last-known-value — no value argument. Costs O((pending + unconditional)-in-subtree).- Return values:
SCHEMA_NOT_FOUND – The sweep ROOT retains nothing (a
HANDLER) — assign’s refusal, at the other half of the pair (RFC-0008 Amendment 2). Only the root is judged: a sweep rooted at a retaining ancestor still walks a subtree containing non-retaining vertices exactly as before.
-
result_t<void> propagate(vertex_handle_t v, emission_mode_t mode)¶
Propagate with an explicit EMISSION MODE (RFC-0025 §4.1.2, Amendment 3 clause 5).
Selection is identical to propagate(vertex_handle_t) in both modes — same
delivery_modegating, same subtree, same drained pending marks. Only the FRAMING differs, andemission_mode_t::PER_VERTEXis exactly the one-arg overload, so the shipped default moves under nobody.Under
emission_mode_t::FOLDthe sweep emits one branch-write frame for the swept subtree instead of RFC-0008 §D’s oneFWD{WRITE}per selected vertex: the RFC-0016POINTtree of the selection, node shape byte-for-byte RFC-0005 §B’s (leadingNAME, optionalVALUE, recursivePOINTsub-branches), the root carrying its own leadingNAMEper §B — the one root asymmetry RFC-0016 §A names between a composed-*read* root and a branch-*write* root. Interior vertices that were not themselves selected appear as value-free skeleton nodes so the tree stays connected; §B calls that a valid no-op node.The TERMINUS is untouched. RFC-0005 §B’s branch-write slicing already hands each covered subscription point the smallest subview covering every value at-or-below it, so a folded frame needs no new decode path — and this door emits nothing a §B decomposer would refuse. It is ONE FRAME PER SUBTREE, never a container across several: two disjoint subtrees are two calls and two frames (the retired-LIST ban, RFC-0005 §E / ADR-0003).
REFUSALS, all before anything is delivered or any pending mark is drained, so a refused fold leaves the sweep exactly as it found it and the caller may retry
PER_VERTEX:TYPE_MISMATCH— a selected vertex’s stored value is not a single trailer-lessVALUETLV. Trailer-carrying nodes are REJECTED rather than silently stripped (§B strictness); this is admissible only because RFC-0025 Amendment 1 moved sample time out of the trailer into payloadTIMEchildren. A selected STREAM vertex refuses here too: its since-flush LIST cannot ride a §B node, which admits at most oneVALUE.BACKPRESSURE— the fold could not be framed within the injected memory seams.SCHEMA_NOT_FOUND— the root retains nothing; the VERB’s refusal, shared with the one-arg overload, so both modes answer alike for the same root.
- Parameters:
v – The sweep root; always delivered, never gated by its own
delivery_mode.mode – The emission mode.
-
retention_t retention(vertex_handle_t v) const noexcept¶
What
vretains (RFC-0028 §5.4): its role’s default unless its vertex_policy_t declared otherwise.
-
result_t<void> set_policy(vertex_handle_t v, vertex_policy_t policy)¶
Apply
policytovWHOLE (RFC-0028 §4.12, D12) — the one owner-side wiring verb that replacedset_retention,set_share_threshold_bytes,set_ring_source,set_delivery_mode,set_app_fieldsandset_app_fields_static.Every member of vertex_policy_t is applied, so a member left at its default resets that property; a member that already holds is skipped, so re-applying a policy costs nothing and a default policy on a fresh vertex allocates nothing. A wiring-time call, like
register_vertex(the “configure before frames flow” contract): changing the ring source drains the ring, switching retention toNONEdrops what is held, and re-declaring an owning field table resets its values to the declared ones.- Returns:
SCHEMA_NOT_FOUNDwhen the policy’s retention is illegal forv'srole (aHANDLERasked to retain, aSTORED_VALUEasked for a ring, aSTREAMasked forLAST) — checked before anything is applied, so a refused policy changes nothing.
-
result_t<std::size_t> ring_reserved_bytes(vertex_handle_t v) const¶
Bytes
v'sreceiver ring currently holds RESERVED against its source — the byte bound’s observable.SCHEMA_NOT_FOUNDon a non-STREAM role, matching history.
-
result_t<std::uint64_t> stream_gaps(vertex_handle_t v) const¶
Shed points on
v'sreceiver ring since registration — the cumulativetr::flow::address_shift_gapcensus (RFC-0025 §4.4: a shed with no accounting is non-conforming).SCHEMA_NOT_FOUNDon a non-STREAM role.
-
result_t<value_ref_t> await(vertex_handle_t v, std::chrono::nanoseconds timeout, std::string_view caller = {})¶
Block until the vertex’s value changes or
timeoutelapses; return the value.The READINESS FORM OF A DATA READ (RFC-0008 §A:
awaitobservesassigns at its own vertex, in the state plane, independent of propagation) — so after a wake the value is served through the SAME ROLE DISPATCH read runs, and the two doors answer alike at the same instant. AHANDLERvertex therefore answers from itson_readseam (RFC-0008 Amendment 2, correcting a wake that used to answerNOT_FOUNDafter the awaited write landed); a handler exposing noon_readstill answersNOT_FOUND, which is the read contract’s own degradation, not await’s.The BRANCH fork read takes is deliberately not mirrored: await watches this vertex’s own write sequence, so a branch vertex hands back its own last-known-value, not the composed subtree fold. The READ gate is checked BEFORE the wait, so a denied caller cannot camp on the condition variable.
- Returns:
The value, or a
status_t(TIMEOUT,PERMISSION_DENIED,NOT_FOUND).
-
result_t<void> arm_await(vertex_handle_t v, await_waiter_t &w, std::string_view caller = {})¶
The non-blocking form of await (ADR-0084): gate
callerfor READ, then arm the one-shot waiterwonvand return at once.wfires on the next change ofv, on the writer’s thread, after the publish has landed; the callee then serves the value with await_value. The graph allocates nothing:wis the caller’s, and must stay alive until it fires or disarm_await returns true for it. There is no deadline here — a timeout is the caller’s to run, by calling disarm_await.- Returns:
PERMISSION_DENIEDwhen the READ gate refuses (nothing is armed).
-
result_t<value_ref_t> await_value(vertex_handle_t v) const¶
Serve a woken await’s value: the role dispatch await runs after its wake, shared so the blocking and the armed forms answer alike.
The READ gate is the caller’s to have checked (arm_await checks it).
- Returns:
The value, or
NOT_FOUND(never assigned / a handler with noon_read).
-
result_t<value_ref_t> read(vertex_handle_t v, const field_path_t &field, std::string_view caller = {}) const¶
Field-read by handle (the read dual of the field-write overload).
An empty
fieldis an ordinary value read — the SAME reference read hands back, no copy; otherwise serve:schema,:acl,:children[](the folded listing) or a single:subscribers[N]slot (the slot’s stored SUBSCRIBER view, zero-copy). For the whole-array:subscribers[]read use read_subscribers. Used by the FWD resolver.One read type (RFC-0028 D11): a field answer is a value_ref_t like every other read. A field value is COMPOSED — nothing published it — so it costs the one block
value_ref_t::composeddraws from the heap; an emptyfieldcosts nothing.
-
result_t<std::vector<view::view_t>> read_subscribers(vertex_handle_t v, std::string_view caller = {}) const¶
Read the
:subscribers[]array — the populated slot SUBSCRIBER views in slot order.Each is a zero-copy refcount clone of the stored source view. The FWD resolver ropes these under a fresh PL=1 wrapper into the REPLY (RFC-0004 §D, no byte copy).
-
result_t<std::size_t> history(vertex_handle_t v, std::span<value_ref_t> out) const¶
Stream history into caller storage, oldest first (Stream role only) — RFC-0028 D11.
Fills
outwith the NEWESTmin(out.size(), retained)ring entries, oldest first, each a value_ref_t share of the retained block: one refcount bump per entry, NO allocation and no byte copy. A span as long as the vertex’sretention_t::Ndepth always holds the whole ring. Entries past the returned count are left untouched.- Return values:
status_t::SCHEMA_NOT_FOUND –
vis not a STREAM.status_t::PERMISSION_DENIED – The local caller lacks READ.
- Returns:
The number of entries written to
out.
-
result_t<std::size_t> drain_unflushed(vertex_handle_t v, std::vector<value_ref_t> &out, std::uint64_t *gap_before = nullptr)¶
Drain
v'sSTREAM entries appended since the last flush, in order — a queue, not a coalesce (RFC-0008 §E) — and advance the drain cursor.The handle-based mirror of the drain half of §E: the same cursor the internal write and sweep paths advance, reachable by an owner that drives propagation itself. It is the observation seam for §E’s “a stream’s flush delivers each ring entry appended since the
previous flush” —
historyshows what the ring RETAINS, this shows what is OWED.The out-param is deliberate, not a returned vector:
vertex_t::drain_unflushed’s #477 nothrow contract is “on OOM return 0 WITHOUT advancing the cursor, so the entries
re-drain on the next covering flush”, and that only holds with caller storage the snapshot can nothrow-reserve into. This form inherits that contract verbatim, including the note that entries trimmed out of the keep-last ring before the drain are lost.
Draining ADVANCES the cursor, so a later propagate sweep will not re-deliver what this took — the caller now owns delivering them.
- Parameters:
v – The STREAM vertex to drain.
out – Caller storage the drained entries are assigned into (overwritten).
gap_before – Optional out: shed points on this ring since the previous drain — the in-order
tr::flow::address_shift_gapsignal of RFC-0025 §4.4/§4.5. Non-zero means entries this consumer would have seen are MISSING immediately before the returned batch. Written whenever non-null, including on a zero drain, so polling a quiet ring still surfaces a shed. Silence is the one behaviour the pressure contract forbids, and this is where it is broken.
- Return values:
status_t::SCHEMA_NOT_FOUND –
vis not a STREAM — no ring, no cursor, the same disposition history gives a non-stream role.status_t::PERMISSION_DENIED – The local caller lacks READ. A drain hands back the SAME bytes history serves, so leaving it ungated would be a READ-gate bypass wearing a different verb’s name.
- Returns:
The number of entries drained (0 ⇒ nothing appended since the last flush, or the snapshot could not be allocated — retry on the next flush).
-
result_t<void> mark_flushed(vertex_handle_t v)¶
Advance
v'sSTREAM drain cursor to “now” WITHOUT draining (RFC-0008 §E) — an eager delivery already flushed the ring, so a later sweep must not re-deliver.The handle-based mirror of the flush half of §E, and the exact verb
write_branchalready uses internally after it fans a decomposed slice out eagerly. Takes no cursor argument: the cursor is the per-vertex “appended since flush” count and the only thing a flush can say about it is “nothing is owed”.Ungated beyond the role check, unlike drain_unflushed — it discloses no bytes and has no wire surface — the same owner-side shape propagate and set_policy carry.
- Return values:
status_t::SCHEMA_NOT_FOUND –
vis not a STREAM.
-
result_t<value_ref_t> read_children_folded(vertex_handle_t v) const¶
FOLDED projection of the
:childrenlisting (L4 fold, Slice 0) — the SAMEPOINT{ POINT{NAME}… }that the materializedread_childrenserializes, but produced as a scatter-gather link chain (an outer POINT header link plus the member links) instead of one flat buffer, answered as a value_ref_t (RFC-0028 D11: every value read answers one type).A read-only projection over the materialized tree — the tree stays the source of truth; this walks it and gathers rather than copying the whole listing into a single allocation.
read_children_folded(v)->flatten()is byte-identical to the materializedread_childrenserialize, whichfolded_children_testgates over many graph shapes. The value is valid while the graph (and its insert-only, pointer-stable vertices) outlive it. The synthesized-listing case (ADR-0044) has nothing to gather and crosses as a single-link value. The composed value is one block from the global heap (value_ref_t::composed), whose refusal isBACKPRESSURE. Each member’s NAME bytes are borrowed IN PLACE (zero copy, view::borrow_const) over the pinned child vertex — only the tiny POINT headers are emitted — so the listing is never copied whole.
-
result_t<value_ref_t> read_children_materialized(vertex_handle_t v) const¶
MATERIALIZED
:childrenlisting — the flat single-link serialize of the samePOINT{ POINT{NAME}… }the fold gathers.The production field read serves the FOLDED value; this flat form exists as the independent oracle
folded_children_testdiffs the fold against (byte identity on flatten() over many graph shapes) — without it the differential would be tautological.
-
result_t<value_ref_t> read_subtree_folded(vertex_handle_t v, std::string_view caller = {}) const¶
COMPOSED BRANCH READ (RFC-0005 §C follow-on): the POINT tree of
v'sregistered subtree, folded as a scatter-gather link chain of views over the live last-known values (zero flatten, zero byte copies), answered as one composed value_ref_t (RFC-0028 D11).composed(target) = POINT{ [stored TLV of target]?, child_node* }andchild_node(c) = POINT{ NAME(c), [stored TLV of c]?, child_node(grandchild)* }— each node’s value is that vertex’s stored TLV verbatim (the landed LKV bytes, opaque: a non-VALUE TLV such as a STATUS composes as-is; descendant HANDLERon_readseams are not invoked). Unregistered placeholders are skipped exactly asread_childrenskips them; synthesizedon_childrentransport listings are not graph children and are absent. A vertex thecallermay not READ prunes its whole subtree (siblings unaffected). A branch with no descendant values folds to a names-only (topology) POINT tree.This is what a plain read serves when the target has ≥ 1 registered child; it is public for the same oracle reason as read_children_materialized’s split. Per node: one atomic
read_stored()load, LKV links refcount-**cloned** (no byte copy), the child’s NAME record borrowed in place over the pinned vertex, and an owned per-level POINT header (opt.llauto-widened at the same 0xFFFF boundary aswire::emit_tlv). The walk is an ITERATIVE stack machine over a HEAP-BACKED stack, so it needs no synthetic cap: the bound is the allocator, and exhaustion isBACKPRESSURE.It does NOT rely on
kMaxSegments, and this comment used to claim it did (“graph depth
is `kMaxSegments`-bounded structurally”). That claim is false:
kMaxSegmentsis enforced only inpath_t::parse(core/src/path.cpp:110), the LOCAL string→bytes builder.ensure_vertextakes raw key bytes and counts nothing, so a write-create already registers a vertex at any depth — locally without limit, and from the wire at whatever depth a branch write’s POINT nesting reaches (RFC-0005 §D amendment 1 took the unresolved-dstarm away, but not decomposition’s landing sites). The iterative walk is safe because it is iterative and resource-bounded — which is the real reason, and the only one that surviveskMaxSegmentsbeing lifted.Resolver contract: with a subject resolver installed,
acl_allows— and therefore the resolver callback — runs O(nodes) times per composed read under the shared ; a resolver MUST NOT re-enter graph mutation APIs (self-deadlock).
-
result_t<void> subscribe(const path_t &src, const path_t &target, delivery_policy_t policy = {})¶
Subscribe
srcto atargetvertex — a write to src re-dispatches the cloned value to target (spec-faithful).NOT_FOUNDif src is unknown.These
subscribe(...)overloads are host SDK sugar, not new wire primitives: the wire data API stays read/write/await (ADR-0006). On the wire, subscription is a consumer-initiated SUBSCRIBER write into the producer’s:subscribers[]field (ADR-0026), exactly as connect() is sugar over that field-write. Per ADR-0049 (#59) this overload ENCODES aSUBSCRIBER{PATH}TLV and enters the same:subscribers[]field-write admission door as a wire subscribe — one parse, one SUBSCRIBE gate, one durability latch, and the edge’s stored SUBSCRIBER view reads back byte-identically from:subscribers[].- Parameters:
policy – This subscription’s DELIVERY policy (RFC-0022 §3.A) — the same packed 16 bits a wire subscriber sends in its
SETTINGSchild, and encoded into exactly that child here so the two doors stay byte-identical. Defaulted to all-zero: best-effort, default priority, no durability request — today’s behaviour for every caller that says nothing.
-
result_t<subscription_t> subscribe(const path_t &src, subscriber_fn_t fn, void *ctx, delivery_policy_t policy = {})¶
Subscribe
srcto an in-process{fn, ctx}callback (sugar; fires inline on each delivery to src with the rope value).The per-edge sink is a plain function-pointer pair (ADR-0047 hot-path shape, like
transport_t::set_receiver), so the per-publish edge snapshot is a trivial copy — nostd::functionclone. Delivery is value-agnostic (RFC-0008): WHICH vertices a sweep propagates is the source vertex’s delivery_mode, not a per-edge policy. A callback cannot ride a TLV, so this overload skips the door’s parse — but it enters the SAME single admission step (SUBSCRIBE gate → append → durability latch, ADR-0049) as every other door.- Parameters:
fn – The per-delivery sink;
ctxis passed back as its first argument.ctx – Caller-owned context, passed back to
fnon every delivery. Its lifetime is bounded by this build’s reclamation policy (ADR-0080), not by prose: keep it alive until unsubscribe releases it — under the default tr::graph::reclaim_local_t that is beforeunsubscribe()returns when called from outside a delivery, and before the enclosingwrite()returns when called from inside one. Pass a tr::graph::subscriber_release_fn_t to unsubscribe(const subscription_t&, subscriber_release_fn_t) to be TOLD which; there is no in-flight state to poll. See subscription_t.policy – This subscription’s DELIVERY policy (RFC-0022 §3.A); defaulted to all-zero, i.e. today’s behaviour. A callback edge carries no TLV, so the policy is set on the slot directly rather than parsed out of one.
- Returns:
A subscription_t handle for unsubscribe; error on an unknown
srcor a denied SUBSCRIBE gate.
-
template<typename F>
inline result_t<subscription_t> subscribe(const path_t &src, F &callback, delivery_policy_t policy = {})¶ Subscribe
srcto a caller-owned callable (sugar over the{fn, ctx}form).Zero-erasure sugar mirroring
transport_t::set_receiver:callbackis bound by address (lvalues only — a temporary would dangle). Its lifetime bound is the{fn, ctx}form’s, since it IS thectx: it must stay alive until unsubscribe releases it, which this build’s reclamation policy (ADR-0080) pins to a moment the library reaches on its own — see subscription_t.- Parameters:
policy – This subscription’s DELIVERY policy (RFC-0022 §3.A); all-zero default.
- Returns:
A subscription_t handle for unsubscribe (as the
{fn, ctx}form).
-
result_t<void> unsubscribe(const subscription_t &sub)¶
Remove the in-process subscription
subreturned by subscribe.The host-SDK-sugar counterpart of the wire
:subscribers[N]clear (ADR-0049): it deactivates the edge slot and unwinds the RFC-0005 listener bookkeeping (descendants’ writes stop bubbling to the producersubnames), exactly as the wire path does. The shell stays (index-stable) and a later subscribe reuses it. Idempotent-ish: a default-constructed or already-cleared handle returnsNOT_FOUND.On the . Retirement takes effect at once — the next snapshot skips the slot — but a fan-out ALREADY walking a snapshot still names the retired
{fn, ctx}pair. Under the default tr::graph::reclaim_local_t there is exactly one case where that can be true of THIS call: unsubscribing from inside a delivery. So when this overload is called from outside any delivery — the ordinary case — it returns already quiescent and the caller may free itsctxon the return. Called from INSIDE a delivery it cannot tell the caller when the pair died, because it was given no way to: use the two-argument overload below, which is the form that carries a signal. Under tr::graph::reclaim_strict_t re-entrant unsubscribe is forbidden outright, so this overload is always quiescent on return.Note
Applies to the callback-form subscriptions; a path→path (
subscribe(src, target)) edge is a wire:subscribers[]field-write, removed via that wire clear.
-
result_t<void> unsubscribe(const subscription_t &sub, subscriber_release_fn_t release)¶
Remove the in-process subscription
suband be TOLD when itsctxis dead — ADR-0080’s event-driven half.Identical to the one-argument overload in what it retires; it adds the one thing that overload structurally cannot provide, a signal.
releaseis invoked exactly once with the subscription’scallback_ctx, on THIS thread, outside every graph lock, at the grace point the bound tr::graph::default_config_t::reclaim_policy_t names:bound policy
called from OUTSIDE a delivery
called from INSIDE one
inline, before this call returns
forbidden
inline, before this call returns
before the enclosing
write()/propagate()returns | | tr::graph::reclaim_qsbr_t | inline when NO participant is mid-dispatch | once every participant has passed a quiescent state — possibly on another thread |So the caller frees its context from
releaseand never asks a question about in-flight state — the library owns that tracking. A hook is run ONLY for a call that actually retired an edge: aNOT_FOUNDreturn (a default-constructed handle, an already-cleared slot) owes no signal and runs nothing.Note
A deferred hook needs one of the tr::graph::default_config_t::kDeferredReleaseSlots parking slots — this thread’s, or under
reclaim_qsbr_tthe shared table’s. If every one is taken the pair is DROPPED and the hook never runs — a deliberate leak in preference to a use-after-free — and deferred_release_drops counts it.- Parameters:
sub – The handle subscribe returned.
release – The release hook;
nullptrdegrades this to the one-argument overload. It must not itself unsubscribe the same handle, and it runs on whichever thread reached the grace point.
- Returns:
{}on success;NOT_FOUNDwhensubnames no active edge — and thenreleaseis not called.
-
result_t<void> set_identity(std::uint8_t kind, std::span<const std::byte> key)¶
Install this NODE’s identity — the key
read <vertex>:identityserves (#406, RFC-0011; ADR-0045 decision 3 “the public key *is* the identity”).NODE-scoped, not per-vertex: a node is one path tree, so EVERY vertex of this graph answers
:identitywith the same byte-identical record. That invariant is the whole point — it is what makes the record a valid CROSS-PATH key, so a client walking/band/c/a/bcan prove they are one device (ADR-0044 point 3: the core never dedups; the client does, keyed by an identity it chooses — this is that key).NO CRYPTO IS INVOLVED HERE, deliberately. The record is a claim: this seam stores and serves bytes the owner supplies and verifies nothing. Proving a node HOLDS the key is authentication (the ADR-0045 challenge/Noise handshake) and lives elsewhere; a claim is nevertheless exactly what a TOFU peer needs to pin, and what a topology walk needs to dedup. Treat an unpinned identity accordingly.
Idempotent and re-callable; the last install wins. CONFIGURATION, like register_child_type — install before frames flow.
Install,
clear_identityandread_identitynevertheless serialize on one lock (#1049), because this is the one member on that list whose READ is served ABOVE the READ gate — an unauthenticated peer may pin the key on first use (RFC-0011 §C), which is deliberate. The read memcpys the stored record, so a rotation racing it would otherwise be a remotely-reachable use-after-free. All three verbs are cold, so the lock is invisible; a runtime rotation is therefore SAFE here, merely outside the doctrine.- Parameters:
kind – The RFC-0011 §B identity-kind (
0x01= ed25519 raw public key).key – The raw public key. Length MUST match
kind(ed25519 ⇒ exactly 32).
- Return values:
TYPE_MISMATCH –
kindis outside the registry (0x00is reserved-invalid), orkey’s length contradictskind.
-
void clear_identity()¶
Drop this node’s identity —
:identityreverts toSCHEMA_NOT_FOUND.The keyless state is the surface being ABSENT, not empty (RFC-0011 §C.3): a node without a keypair genuinely has no identity facet, which is the
ENOTTYof an unsupported field, byte-for-byte the pre-RFC behaviour.
-
void set_hooks(const graph_hooks_t &hooks) noexcept¶
Replace the graph’s five wiring seams WHOLE (RFC-0028 §4.12, D12) — the one verb that replaced the five
configure_*verbs.The constructor takes the application’s hooks; this verb exists for the party that can only be built AFTER the graph —
tr::net::fwd_router_t, whose constructor takes the graph and installs its three transport-plane seams (remote_delivery,wire_target,stats_sampler) by reading hooks, filling its slots and handing the whole struct back, so the application’s two stay as they were.CONFIGURATION, not a runtime knob (#1049): from one thread, before frames flow. Each slot is published through its own
tr::sink_slot_t, so a dispatch racing the install sees a whole new pair, a whole old one, or none — never a newfnbeside a stalectx; what it does NOT do is stop a dispatch already in flight, so everyctxmust outlive every dispatch that can still reach its seam.
-
graph_hooks_t hooks() const noexcept¶
The five wiring seams as currently installed — the read half of set_hooks (one coherent snapshot per slot).
-
bool sample_stats(std::string_view seam_class, std::string_view seam_name, stats_block_t *out) const noexcept¶
Ask the installed sampler for one net-plane seam — the read side of graph_hooks_t::stats_sampler.
Public because the census encoder is a free function over
graph_t’s public accessors (it needs no friendship and mints no other symbol), and useful on its own to an in-process supervisor that wants one seam’s block without going through the wire door.- Parameters:
seam_class – The seam CLASS (
router,labels,link).seam_name – The seam NAME within it.
out – Where to write the block, or
nullptrto probe recognition only.
- Returns:
falsewhen no sampler is installed or the spelling names no seam.
-
result_t<void> subscribe_wire(vertex_handle_t v, view::view_t source_view, view::view_t return_route, std::string link, view::view_t reverse_route = {}, std::string caller = {}, link_id_t link_token = {})¶
The wire
:subscribers[]APPEND — the same admission door as the local sugars and field-writes (ADR-0049), plus the remote delivery binding.Called by the FWD resolver on an inbound
:subscribers[]WRITE (#59/#136); it replaces the retiredadd_remote_subscriberparallel API.source_view(the SUBSCRIBER TLV, an owned copy) is parsed ONCE here — thedelivery_compactopt-in comes from this parse (the resolver no longer parses it in parallel) and the view is retained zero-copy so a:subscribers[]read serves it back. A PATH child, if present, names the consumer at ITS origin and is deliberately NOT bound as a local re-dispatch target — remote delivery ridesreturn_route(a view over a refcounted segment — the ONE copy of the route; every later delivery clones the refcount, ADR-0041 §2) overlinkvia the remote sink. Admission is the single ADR-0049 step: SUBSCRIBE gate onv's:aclunderlink(#81, ADR-0026,PERMISSION_DENIEDon denial) → slot append → durability latch (if the parseddelivery_policy_tsetsdurability_requestandvholds a value, the LKV is latched to this subscriber — one synchronous sink call, RFC-0004 §D / RFC-0022 §3.A).return_routeMUST be non-empty — an empty one isINVALID_PATH(#1055). This door is the only one that binds a link for delivery, so it is where the two fields are held to ONE meaning: an edge that carries a link carries the route to deliver over it. The fan-out body (dispatch_edge) therefore tests the link alone and hands the sink the route unchecked, which is what keeps that deliberately-inlinable per-edge test at one comparison; admitting a routeless edge instead bought aFWD{WRITE}with a zero-bytedston every publish. Both in-tree callers already satisfy this (the resolver rejects a failed route copy asBACKPRESSURE,fwd_router_t::subscribe_towardrefuses an empty residual asINVALID_PATH), so the door narrowed to what the wire already produced.reverse_route, when non-empty, is the COMPLETED reverse-direction bound route (RFC-0024 §7.1 amendment 1): aPATH_REFTLV whose element 0 is THIS node’s own reference to the connection vertex the subscribe arrived on, followed by the elements the forwarding hops contributed. Stored besidereturn_routeas the delivery optimisation + liveness check; empty (the default, and every pre-amendment caller) keeps the subscription canonical-only, byte-identical to before.calleris the SUBJECT this admission is gated under and the fan-in context every later delivery through the edge re-gates under — who subscribed, as againstlink'swhere to deliver (ADR-0082 §Decision 1; the two were one string until #375 Part 2). EMPTY means “the same as @p link”, which is every pre-split caller and is byte for byte what this door did before: a FLAT listener’s peers all subscribed as their shared link name. A terminus that derived a per-writer subject from the frame’speer_handle_tpasses it here, and then the SUBSCRIBE gate, the storedsubscriber_remote_t::callerand each delivery’s WRITE re-gate all name the writer rather than the wire it used. The delivery link is unaffected, which is the point of splitting them: the edge still routes back overlink(or, for a mount-routed target, over the mount).link_tokenis the CARRIED interned identity oflink(#1266 / #1417) — what the transport plane got back from intern_link at link-up and cached in its own per-link receive context. It saves the index the name hash and find — 42–56 % of the whole index operation net of its control, measured at all ten cells of #1416’s own harness. A token that cost NOTHING to obtain would be worth 72–82 %, so roughly two thirds of that ceiling survives paying for the carry. It is an OPTIMISATION and only that: the default is “no token”, which is byte-identical to every pre-carry caller, and a token that does not name a slot holding the key the index is about to use is ignored rather than trusted. That is deliberate — the key is not alwayslink(a mount-routed target rebinds it to the mount’s, and afield_writeadmission has only itscaller, #943), and a token silently indexing under the wrong link is exactly the leaked subscriber edge #1071 exists to prevent.
-
result_t<value_ref_t> read(const path_t &path) const¶
Read by path — resolve the path key once (guarded map lookup), then the hot path.
A read whose path has a field tail (e.g.
:settings.app.kp,:subscribers[],:schema) is routed to the field surface.
-
result_t<void> write(const path_t &path, view::rope_t value)¶
Write by path — resolve the key once, then write(vertex_handle_t, view::rope_t,std::string_view, const net::link_kind_t*).
-
result_t<value_ref_t> await(const path_t &path, std::chrono::nanoseconds timeout)¶
Await by path — resolve the key once, then await(vertex_handle_t,std::chrono::nanoseconds, std::string_view).
-
std::optional<vertex_handle_t> find(std::span<const std::byte> key) const¶
Resolve a canonical PATH-payload
keyto its vertex handle (nulloptif unknown).
v'scopy-or-share threshold in bytes (RFC-0028 §5.3): what it declared with vertex_policy_t::share_threshold_bytes, else tr::graph::config_t::kShareThresholdBytes.The read accessor the opaque handle does not expose directly: the WRITE resolver (
op_resolve_walk.hpp) queries it here instead of dereferencing the vertex. One inline load, and nothing is inherited (RFC-0022 §3.F).
-
result_t<vertex_handle_t> ensure_vertex(std::span<const std::byte> key, std::string_view caller = {})¶
Find-or-create the vertex at
key(write-creates, RFC-0005).Resolves
key; when absent, creates the vertex — and every missing intermediate level,mkdir -pstyle, each a STORED_VALUE vertex — gated by the CREATE right on the nearest EXISTING ancestor’s effective ACL undercaller(PERMISSION_DENIED when denied; a graph holding no ancestor at all is open, matching ACL-presence opt-in). A creation race lost to a concurrent caller is benign (the winner’s vertex is returned).keymust be a well-formed, non-empty canonical PATH-payload (else INVALID_PATH).Note
This is the LOCAL creation door and the branch-write decomposition’s landing door — NOT the remote miss handler. Since RFC-0005 §D amendment 1 (#1139) a peer’s fieldless
FWD{WRITE}to an unresolveddstanswers NOT_FOUND and never reaches here; a peer creates through the ADR-0059 creator endpoint. The asymmetry is deliberate: the in-process caller owns its graph’s structure.Note
No SCRATCH allocation. The level walk stores nothing and the registration takes borrowed key bytes, so the per-call temporaries that used to scale with the key’s DEPTH — the part a peer chose the size of — no longer draw from the global heap behind the injected
block_source_t’s back (#1139, #873). Thevertex_tobjects themselves are still heap-allocated; that is the larger #873 arena question.
-
result_t<void> hide_from_enumeration(vertex_handle_t vh)¶
Drop
vhout of its parent’s:children[]listing, keeping it registered and addressable — the enumeration-hide seam RFC-0014 §3 requires (stage S4 of #492).The one caller today is tr::net::transport_vertex_t, on the creator endpoint
<net_root>/<module>/conn: §3 reserves that name and says it is hidden from<net_root>/<module>:children[], which “returns the member connection vertices”. The endpoint is not one of them — it is the write-only control that CREATES them — so a peer walking the listing as a topology of links descends into a vertex with no peer behind it. Discovery of the endpoint is by §6’s creatability probe (read <module>/conn:schema) instead, which is why hiding must not cost addressability.Scope, deliberately: this changes
:children[](both the materialized and the folded door) and nothing else. find still resolves the vertex, reads and writes still reach it, the RFC-0016 composed branch read still descends into it, and the owner-side for_each_vertex census still visits it. RFC-0014 §3’s clause is about the member listing; widening it to the other surfaces is not this seam’s call to make.One-way. The bit belongs to the current occupant of the key, so a retire clears it along with the rest of that occupant’s identity; a fresh registration is listed again unless it hides itself. There is no
unhide.- Parameters:
vh – The vertex to hide. Must be registered.
- Return values:
status_t::NOT_FOUND –
vhis null or is an unregistered placeholder — hiding a vertex that is not a member yet would silently apply to whatever registers there later.
-
std::uint64_t ancestor_walks() const noexcept¶
How many writes performed the ancestor (bubbling) walk — instrumentation.
The near-free-when-idle observable (RFC-0005): stays 0 while no subscriber exists above any written vertex, so tests and benches can assert a write never walks ancestors unless someone is listening. Relaxed monotonic counter.
Compiled in only when default_config_t::kInstrumentCounters is bound
true; on a lean build (the default) the counter does not exist and this answers0(#1664).
-
std::uint64_t target_canonical_resolves() const noexcept¶
How many target-edge deliveries fell back to the canonical
find_ptrwalk instead of the minted binding (#830) — instrumentation.The inverse of a hit counter ON PURPOSE: this is the only path #830 leaves paying the O(depth) resolve, so counting it costs the fast path nothing at all — no atomic on the bound leg. Non-zero means one of: the edge was admitted before its target existed (or against a placeholder / saturated generation, so no mint was possible), or the binding went stale and
deref_vertex_slotrefused it. Relaxed monotonic counter, and the observable an ablation uses to prove the bound leg is the one actually running.Compiled in only when default_config_t::kInstrumentCounters is bound
true; on a lean build (the default) the counter does not exist and this answers0(#1664).
-
delivery_drops_t delivery_drops() const noexcept¶
Snapshot the per-cause delivery-drop counters (delivery_drops_t).
The loads are individually relaxed and not one atomic snapshot, so a reader racing a delivering thread may see a torn total. That is deliberate: making it coherent would put a lock on the drop path to serve a diagnostic, and these are monotonic counters whose useful reading is “is this growing”, not an instant.
-
void count_external_drop(external_drop_t why, std::uint64_t n) noexcept¶
Count
ndeliveries an off-graph deliverer declined, into delivery_drops.The ONE public door to the drop counters (#1068). The net plane performs deliveries the graph never sees — a COMPACT terminus resolves a label to a vertex and writes it — so the drops on that path are invisible to every counting site inside
graph_t. This is a method rather than a friendship because the counters are a public, documented surface while the internal drop sites are not: a deliverer needs to add to the published numbers, not to reach into the machinery that maintains them.nis a delivery count, never an event count, exactly as for the internal sites: a deliverer that sheds N deliveries counts N. Relaxed monotonic; costs nothing when nothing is dropped.
Public Static Functions
-
static bool disarm_await(await_waiter_t &w) noexcept¶
Take back an armed waiter that has not fired (a timeout or a teardown).
- Return values:
true – It had not fired, and it never will: it is the caller’s again.
false – It already fired (or is firing) on a writer’s thread.
-
static std::uint64_t deferred_release_drops() noexcept¶
How many retired
{ctx, release}pairs this PROCESS has dropped for want of a parking slot — each one a release hook that will never run (ADR-0080).The observability half of the bounded park: a leak is the safe answer to an exhausted bound, but a silent one is not. A non-zero reading means tr::graph::default_config_t::kDeferredReleaseSlots is undersized for how many subscriptions this node retires from inside a single delivery stack — raise it in the override fragment. It is process-wide (summed across threads) and monotonic, and it stays 0 forever on a node that never unsubscribes re-entrantly, which is most of them.
Under tr::graph::reclaim_qsbr_t it counts the SHARED retired table’s drops and means something sharper: that domain republishes and rescans before it gives up, so a non-zero reading says a participant thread genuinely never reached a quiescent state while the table filled. That is an embedder defect — a dispatching thread that never returns to its event loop, or more concurrent dispatchers than tr::graph::default_config_t::kQsbrParticipants — and this counter is how it surfaces.
-
static inline void thread_quiescent() noexcept¶
Declare that this thread holds no in-flight delivery — the OPT-IN quiescent point of a cross-thread grace period (ADR-0080, #1376).
No policy’s guarantee depends on an embedder calling this
, and that is deliberate: it would otherwise contradict ADR-0080 §Decision 4 and the reference article’s “there is no
verb the embedder must remember to call”. Every dispatching thread announces and drains AUTOMATICALLY at its outermost dispatch exit, which already covers ADR-0080 §Decision 3’s named case — an RX thread that has finished a message and returned to its event loop.
It exists for the thread the automatic path cannot reach: one that mutates LKV slots (displacing nodes onto its own private retired list) without ever dispatching, and one that wants a stated teardown precondition before an injected
std::pmrresource dies (ADR-0039 §Erratum 8’s “domain quiescence point”, which #897 asks be nameable). Call it at the top of an event loop, or once before joining such a thread.Under the two per-thread policies it is an empty inline function — it compiles to literally nothing, and
graph.cppis not even aware of it. It must NOT be confused with collect, which is the ADR-0072 value-seam park and answers an unrelated question; this verb is not a poll of in-flight state and returns nothing.
Public Static Attributes
-
static constexpr std::size_t kNoVertexCeiling = static_cast<std::size_t>(-1)¶
set_vertex_ceiling’s “no ceiling” value, and its default.
-
struct delivery_drops_t¶
Why a delivery was declined, counted per cause.
A path-target edge — the form a wire
SUBSCRIBERproduces, naming a target PATH — delivers by re-dispatching into that target. Three conditions make that impossible, and all three are specified to DROP the one delivery rather than fail the write: the write itself succeeded and the other legs still ran. Dropping is correct. Dropping invisibly is what these counters fix — a node whose target was retired, or whose fan-in gate denies the edge’s stored caller, otherwise drops every delivery for the rest of its life with nothing anywhere to say so.Two counters reach past that edge, because the same blindness was reachable from the net plane (#1068). A COMPACT terminus delivery is a write like any other, and the router that performs it discards the status: an
:aclthat refuses the inbound link, a route that no longer resolves, or an allocation that fails under pressure each shed a frame that an operator could not see. count_external_drop is the door that plane counts through, and denied is counted at the graph’s own WRITE gate so it is one number for every plane rather than one per deliverer.The drop is not always ONE delivery, and the counters say so by counting deliveries rather than events (#896): a fan-out truncated by an unreservable overflow buffer sheds every edge past the inline prefix, and an
assignwhose pending mark cannot be allocated sheds the vertex’s WHOLE subscriber set — each shed delivery is one increment, so1never stands in forN. A handler write’s notify clone used to be the widest of these; #1505 deleted the clone rather than the tally, so that shed is now impossible rather than counted, and the width rule is unchanged by its removal.Counted, never enforced: nothing in the library reads them, so a deployment chooses whether to alarm. Relaxed monotonic, incremented only ON a drop — the delivering path pays nothing when nothing is dropped, exactly like ancestor_walks.
Public Members
-
std::uint64_t no_target = 0¶
The target PATH resolved to no live vertex (retired, or never created) — a subscription edge’s target, or a net-plane route that no longer names one (count_external_drop).
-
std::uint64_t denied = 0¶
A WRITE was refused by the target’s
:acl(#81, #1068). Counted on EVERY plane the value-write path is entered from — an APIwrite, a FWD{WRITE} terminus, a COMPACT terminus, and a subscription edge’s fan-in gate — so this is “refusals”, not “refusals nobody was told
about”: an API caller both receives
PERMISSION_DENIEDand counts here. Deliberately NOT counted:assign(the no-delivery state half), a control-plane field write, and a denied READ — each a different right or a different path, and folding them in would make one number mean four things.
-
std::uint64_t out_of_memory = 0¶
The nothrow delivery clone / edge-view copy could not be allocated (#477) — one count per delivery shed, whatever the fan-out width.
-
std::uint64_t fan_out_truncated = 0¶
Deliveries shed because a wide fan-out’s snapshot could not be widened past the inline prefix — a capacity degrade, not an allocation failure on the delivery itself (
vertex_t::snapshot_drops_t::truncated).
-
std::uint64_t no_target = 0¶
-
struct session_anchor_route_t¶
The (mount, peer) a SESSION ANCHOR names, or nullopt for every ordinary vertex — the reverse-list delivery’s egress question (RFC-0024 §7.1 amendment 1, #1223).
A bound delivery’s LAST element dereferences to the accepted session’s identity vertex (the #1254 anchor); the hop that consumes it must egress to the SESSION, and this is where it learns which one. Classification is by the anchor’s own key shape — the id is
:<mount>/<peer>, and both:and/are characterspath::valid_segmentforbids, so no addressable vertex’s key can ever satisfy it (the same argument that makes the anchor unspellable makes this test unforgeable). The views are BORROWED from the vertex’s immutable key record and stay valid for the graph’s life (vertices are never freed).
-
enum class external_drop_t : std::uint8_t¶
-
class vertex_t¶
An L4 graph vertex: a named, addressable position holding a value, a bounded history, or a user handler (docs/reference/11 §roles).
Pinned in place (the atomic last-known-value slot + mutex + condvar are non-movable) and always handled via a
vertex_handle_treturned bygraph_t::register_vertex(ADR-0056). The read/write hot path takes no vertex lock (an atomic shared_ptr swap); the mutex guards only the history ring, the subscriber list, and the await waiter accounting. Non-copyable.The public surface is a VERB interface — reading the stored value (read_stored), readiness (note_write / wait_for_change / the seq cursors), edges (add_edge / clear_edge / snapshot_edges), and ACL state (set_acl / with_acl / with_aces / with_effective_aces) — each verb taking the vertex mutex internally (the LKV slot stays lock-free).
graph_tkeeps what SPANS vertices: routing, ancestor walks, fan-out dispatch legs, the effective-ACL walk, admission, and the field surface.The PUBLISHING half of storage is not on that surface:
store, like the map-lock mutators, is private withgraph_tas its sole friend (#867, #1300), because every publish must passgraph_t::store_value— the one seam that gates the write, injects the ADR-0039 resource and counts what the publish shed. Owners reach the stored value’s queue semantics throughgraph_t::history/graph_t::drain_unflushed/graph_t::mark_flushedinstead.Public Types
-
enum class edge_replace_t¶
Outcome of replace_edge — tells the caller which bookkeeping it owes.
Values:
-
enumerator OUT_OF_RANGE¶
No slot
idxexists; nothing was written.
-
enumerator FILLED_EMPTY¶
The slot existed but was cleared — this is an ADD.
-
enumerator REPLACED_ACTIVE¶
A live edge was swapped out; the listener count is unchanged.
-
enumerator OUT_OF_RANGE¶
-
enum class app_read_t¶
One app-field read outcome — the graph maps these onto the RFC-0002 identities (
SCHEMA_NOT_FOUND/NOT_FOUND).Values:
-
enumerator UNDECLARED¶
No such field in the table (or no table) —
ENOTTY.
-
enumerator WRITE_ONLY¶
Declared
wo— no read surface (RFC-0010 §A.4).
-
enumerator UNSET¶
Declared but never written and no initial value.
-
enumerator OK¶
Value copied out.
-
enumerator UNDECLARED¶
Public Functions
-
inline vertex_t(role_t role, path_key_t name, handlers_t handlers, tr::mem::block_source_t &src = tr::mem::table_source())¶
Construct a vertex with its role, own canonical NAME record (ADR-0057 — one segment, not the full key), and handlers. The cold extension block is allocated, from
src, only if this identity needs one (#361 §1).The graph builds every vertex as a handler-less placeholder (no allocation) and installs an identity through
fill, whose refusal is a value. A standalone vertex built WITH handlers whosesrcrefuses the extension block stops the node (mem::exhausted_at_init): a constructor has no other way to answer.
-
inline ~vertex_t()¶
Free the cold extension block (allocated at most once, ADR-0057 lifetime) and flush the edge block’s published + parked arrays (
edge_block_t’s destructor states the outlive-the-publishers contract that makes this safe).
-
inline role_t role() const noexcept¶
This vertex’s behavioral role.
A RELAXED atomic load (#1477). The read is lock-free and on the write hot path, while the writers (
fill, revert_to_placeholder) run under the graph’s unique map lock — so the load is unordered by construction and may observe either the retiring occupant’s role or the placeholder default. What the atomic buys is that the race is WELL-DEFINED rather than UB; it does NOT serialise a write against a retire (see the doctrine note atgraph_t::write).
-
inline const path_key_t &name() const noexcept¶
This vertex’s own canonical NAME record (its single path segment, ADR-0057); empty at the root. The full key is a parent-walk concatenation (
graph_t’stry_build_key).
-
inline bool has_extension_block() const noexcept¶
Whether the lazily-allocated cold extension block EXISTS on this vertex (#361 §1).
A RAM-census observable, not a data-plane predicate: it is the one fact about the pay-for-what-you-use split that no functional surface reveals, and
bench_qos_censusplus the RFC-0022 host tests are its only callers. It used to be spelled by comparingsettings()’s returned ADDRESS against the sharedkDefaultSettingsconstant; RFC-0022 deleted both, so the question needs a name of its own rather than an idiom.
This vertex’s copy-or-share threshold in bytes (RFC-0028 §5.3):
config_t::kShareThresholdBytesunless it declared its own.ONE inline load and nothing more: it is read on EVERY view-delivered write (
op_resolve_walk.hpp), so it may never become an ancestor walk. Nothing is inherited (RFC-0022 §3.F) — a vertex that was never given a threshold answers the build’s default, whatever its ancestors hold.
-
inline const value_handlers_t &handlers() const noexcept¶
This vertex’s user handlers (Handler role behavior + the
on_childrenseam); an all-empty shared constant when no extension block.
-
inline app_field_write_hook_t on_app_field_write()¶
A copy of this vertex’s owner apply seam (RFC-0010 §A.3), or empty when none — taken under the vertex lock so the caller can fire it OUTSIDE the lock (the seam may re-enter the graph). Two words; copying it allocates nothing. Empty ⇒ declared field writes just store.
-
inline vertex_t *parent() const noexcept¶
The owning parent node (
nullptronly at the graph root).Note
Immutable once linked — safe to walk without any lock.
-
inline bool registered() const noexcept¶
True once a registration filled this node; false for a placeholder — a structural intermediate level that
find/read_childrenmust not surface (matching the flat-map behavior where missing intermediates did not exist).Note
Read/written under the graph’s map lock.
-
inline bool enumerable_member() const noexcept¶
True iff this vertex is a
:children[]MEMBER of its parent — registered and not enumeration-hidden (RFC-0014 §3, S4).The enumeration-hide seam RFC-0014 §3 says “the implementation must add”: the RFC-0014 creator endpoint
<net_root>/<module>/connis a real, registered, addressable vertex —findresolves it, aSPEC/NAMEwrite executes on it,conn:schemareads it — but it is NOT one of the module’s connections, and a topology walker that treats every:children[]member as a link descends into a control vertex with no peer behind it (the concrete bug the TypeScript client had to work around in #1302).Deliberately NARROWER than “invisible”: hiding is a member-listing property only.
registered()— which governsfind, retirement walks, the branch/leaf fork (has_registered_child) and the owner-sidefor_each_vertexcensus — is untouched, because RFC-0014 §6 makesread <module>/conn:schemathe sanctioned creatability probe: the endpoint has to stay addressable precisely BECAUSE it is unlisted.Note
Read/written under the graph’s map lock, like registered.
-
inline std::uint32_t retire_gen() const noexcept¶
This vertex’s retirement generation (ADR-0062).
Bumped every time retirement re-virginizes this object, so a holder of a CACHED resolution can tell “the same vertex” from “the same address, a new occupant”. A handle alone cannot: the vertex map is pinned and insert-only, so a stale handle stays usable and would silently address the revived path’s new owner.
-
inline void refresh_registered_child() noexcept¶
Recompute
flag_t::REGISTERED_CHILD. Unique-map-lock callers only.
-
inline bool has_registered_child() const noexcept¶
True iff at least one DIRECT child is registered — the branch/leaf fork of the plain read surface, answered without taking the graph’s map lock (#652).
This used to be
graph_t::has_registered_child, which tookmap_mutex_shared and walked the child list to compute the same predicate. That lock was the single largest term on the read path and, being process-wide, it capped every read in the process at roughly 20 M/s no matter how many cores or how disjoint the vertices: short-circuit it and twenty-four readers on distinct vertices go from 19.7 to 165.3 M ops/s. A blocking lock does not collapse the way a spin lock does — it plateaus — which is exactly why this was invisible for so long: a flat aggregate reads like “scales fine” until you notice that flat across a 24x thread range means each thread is 24x slower.The counter is mutated only by
fillandmark_unregistered, both of which run under the graph’s UNIQUE map lock, so mutations are already serialized; the atomic is what makes the read race-free. A reader concurrent with a registration may observe either side of it — exactly as it could when the fork took a shared lock, since the API orders areadagainst a concurrentregister_vertexno more strongly than this. The composed branch read re-acquires the map lock for its own walk, so the ordering that walk depends on is not this counter’s to provide. One observable follows from that gap: a read racing the retirement of the LAST registered child may see the bit set here and then find no registered child under the walk’s lock, composing the root alone — a POINT with zero child records. That reply is LEGAL, byte-identical to the fully READ-ACL-pruned reply RFC-0016 §B produces deterministically (erratum 2026-08-13, #1030) — a transient in frequency, not a new shape.
-
inline vertex_t *child_by_record(std::span<const std::byte> record) const noexcept¶
The child whose own NAME record equals
recordbyte-for-byte, ornullptr— one level of the O(segments) resolution walk (ADR-0057).Note
Called under the graph’s map lock (shared suffices).
-
template<typename F>
inline void for_each_child(F &&f) const¶ Run
fover every child (placeholders included), in sorted name-record order — member enumeration and the RFC-0005 subtree-counter walks.Note
Called under the graph’s map lock (shared suffices);
fmust not mutate the tree.
-
template<typename F>
inline void for_each_descendant(F &&f)¶ Run
fover every DESCENDANT (this vertex excluded), pre-order, iteratively.The subtree counterpart of for_each_child, and the reason it exists is stack safety: the four subtree walks in
graph.cppwere self-recursion, one frame per graph level at 32–208 B a level, and graph depth is a vertex’s path segment count — which nothing on the wire path bounds.kMaxSegmentsis enforced only inpath_t::parse, the local string builder;graph_t::ensure_vertextakes raw key bytes and counts nothing, so a peer could already create a vertex deep enough to overflow the stack of a walk it then triggers (:subscribers[], RETIRE,:acl). See #690.Descends with no auxiliary storage at all — no explicit stack, so nothing to allocate and nothing to fail. It ascends via the parent link and re-finds its position among its siblings by binary search on its own NAME record, which the
sortedlist already supports (the samelower_boundchild_by_recorduses). That costs O(log children) per ascent instead of the O(1) an explicit stack would give, and buys back an error channel the twovoidcallers could not have carried without a signature change.Note
Same contract as for_each_child — called under the graph’s map lock, and
fMUST NOT mutate the tree — the walk holds no snapshot and re-readssortedon every ascent, so an insertion mid-walk would move the position it is about to resume from.
-
inline void note_write()¶
Record a Handler-role write: bump the write sequence and wake awaiters (the vertex stores no value — the user handler consumed it).
-
inline void arm_waiter(await_waiter_t &w) noexcept¶
Arm the one-shot waiter
won this vertex (ADR-0084): it fires on the next change, on the writer’s thread, instead of blocking the caller.The non-blocking twin of wait_for_change, and the same half of the Dekker pair documented on
store. The waiter count is raised (seq_cst) under the stripe mutex before this returns, so a publish ordered after the arm takes the slow path and findsw, and a publish ordered before it is a change that preceded the wait — exactly what wait_for_change’s snapshot treats as “before”.- Parameters:
w – An unarmed waiter whose
fireis set; it must stay alive until it fires or disarm_waiter returns true for it.
-
inline value_ref_t read_stored() const¶
The stored last-known-value (lock-free; null ⇒ never assigned / Handler role).
-
inline bool wait_for_change(write_seq_t seq0, std::chrono::nanoseconds timeout)¶
Block until the write sequence moves past
seq0ortimeoutelapses.- Parameters:
seq0 – The current_seq snapshot the caller waits to see surpassed.
timeout – The maximum wait.
- Returns:
true iff a change was observed (
write_seq_ != seq0: an equality test, so a wrap between the snapshot and the check is still a change); false on timeout.
-
inline write_seq_t current_seq() const¶
The current write sequence (bumped per assign — the await predicate base). 32-bit and wrapping (
write_seq_t): compare it for equality only.
-
inline void mark_flushed()¶
Advance the STREAM drain cursor to “now” WITHOUT draining (RFC-0008 §E): an eager delivery already flushed the ring, so a later sweep must not re-deliver.
-
inline std::size_t drain_unflushed(std::vector<value_ref_t> &out, std::uint64_t *gap_before = nullptr)¶
Drain the STREAM ring entries appended since the last flush, in order — a queue, not a coalesce (RFC-0008 §E) — and advance the drain cursor.
Snapshots under the lock into
out(caller storage; overwritten); the caller delivers OUTSIDE the lock. Entries trimmed out of the keep-last ring before this drain are lost (bounded history). The snapshot growth is NOTHROW (#477): on OOM the drain returns 0 WITHOUT advancing the cursor, so the entries re-drain on the next covering flush — deferred, never lost, never an abort.Note
#981 residual: “never an abort” holds where the growth THROWS. Under
-fno-exceptionstr::detail::try_reservedegrades to probe-then-commit — the probe block is freed beforereservetakes one and a context switch in that window makes thereserveabort() the node (#850). The snapshot cannot take the ADR-0065tr::mem::block_array_tseam: its element is astd::shared_ptr, which the seam’s memcpy relocation would tear, andoutis caller storage of a type this signature fixes.Note
Counts ring APPENDS, never a
write_seq_delta (#925): that sequence is the await/readiness cursor and bumps on a SHED append too, so the surplus would re-take an ALREADY-FLUSHED entry — a drain removes nothing from the ring.- Parameters:
out – Caller storage the drained entries are assigned into (overwritten).
gap_before – Optional out: shed points observed on this ring since the previous drain — the in-order
tr::flow::address_shift_gapsignal of RFC-0025 §4.4/§4.5. Non-zero means entries the consumer would have seen are MISSING immediately before this batch. Written unconditionally when non-null, including on the zero-drain returns, so a consumer that polls a quiet ring still learns about a shed.
- Returns:
The number of entries drained (0 ⇒ nothing appended since the last flush, or the snapshot could not be allocated — retry on the next flush).
-
inline std::size_t history_into(std::span<value_ref_t> out)¶
Copy the NEWEST
min(out.size(), ring count)STREAM ring entries intoout, oldest first — each avalue_ref_tshare of the entry’s block (one refcount bump, no byte copy, no allocation; RFC-0028 D11).- Returns:
The number of entries written;
out[0, n)holds them, the rest is untouched.
-
inline std::size_t ring_reserved_bytes() const¶
Total bytes this receiver currently holds RESERVED against its injected ring source — the byte bound’s observable (RFC-0025 §4.6.1 clause 3). Zero on a vertex that has never admitted an entry.
-
inline std::uint64_t ring_gap_count() const¶
Shed points on this receiver’s ring since registration — the cumulative
tr::flow::address_shift_gapcensus (RFC-0025 §4.4: a shed with no accounting is non-conforming).
-
inline std::size_t add_edge(subscriber_t s, edge_latch_t *latch = nullptr, tr::mem::block_source_t &tables = tr::mem::table_source())¶
Append a subscription edge; atomically snapshot the transient-local durability latch when
latchis non-null.Under ONE lock hold: the slot is appended, and — iff THIS subscriber requested durability (
policy.durability_request(), RFC-0022 §3.A) and the vertex already holds an LKV — the value plus the new edge’s dispatch view are snapshotted intolatch, so a concurrentclear_edgecan never slip between append and latch. The caller dispatches the latch OUTSIDE the lock (RFC-0004 §D / ADR-0049).The predicate is the SUBSCRIBER’s, not the vertex’s: before RFC-0022 one
settings.durabilityflag latched for every subscriber of a vertex, including the ones that never asked.An INACTIVE slot is REUSED before the list grows (RFC-0009 §D.2: “a cleared
slot MAY be reused by a later append”) — the reclamation half of eviction: a churning link (unsubscribe / peer departure, then re-subscribe) reoccupies its freed slots instead of growing
subs_without bound. Slot indices of ACTIVE edges are never renumbered (§D.2 stability); only a slot already cleared can come to mean a new edge. The edge is PUBLISHED under the same lock hold, in the position the slot append itself holds (#635): the caller’sown_subs_seq_cst bump still precedes this whole verb, so ADR-0049’s Dekker pairing against a skipping publisher is byte-for-byte the one #708 landed. Displaced arrays are scanned AFTER the lock is dropped, on this thread.- Returns:
The occupied slot’s index (the
:subscribers[N]slot number), or kNoSlot when the edge array could not be allocated — nothing was admitted and the previously published array is untouched, so the vertex is unchanged.
-
inline bool clear_edge(std::size_t idx, void **retired_ctx = nullptr, remote_ptr_t *retired_remote = nullptr)¶
Deactivate the edge slot
idx(unsubscribe — a cleared:subscribers[N]).- Parameters:
retired_ctx – Optional out-parameter receiving the slot’s
callback_ctx— the one leg of a dispatch snapshot the library holds no owning copy of, so ADR-0080’s reclamation seam needs it back to hand to a release hook. Read UNDER the lock, before the shell displaces the slot, because after that the pair is gone. Written only on thetruereturn; left untouched when nothing was cleared.retired_remote – Optional out-parameter receiving the cleared edge’s cold
remotehalf (#1816) — moved out under the lock, so the caller can name the link the edge was routed through after releasing it. Same write rule asretired_ctx.
- Returns:
true iff the slot existed and was active (the caller then adjusts the RFC-0005 listener bookkeeping).
-
inline edge_replace_t replace_edge(std::size_t idx, subscriber_t s, edge_latch_t *latch = nullptr, remote_ptr_t *displaced_remote = nullptr)¶
Replace the edge occupying slot
idx(RFC-0009 §D.1), snapshotting the transient-local durability latch under the SAME single lock hold as add_edge.§D.1 makes an indexed
:subscribers[N]write of aSUBSCRIBERreplace that slot rather than destroy it. The latch is taken here, not by the caller, for the reason add_edge gives: a concurrent clear_edge must not be able to slip between the write and the snapshot. The caller dispatches OUTSIDE the lock.Move-assigning the slot reclaims the displaced edge’s
source_viewsegment pin and coldremotehalf in place, exactly as clear_edge does — a replace must not leak the frame segment the old edge pinned.This never grows . An out-of-range
idxis refused rather than back-filled with inactive shells: the index arrives off the wire, so growing on demand would let a peer allocate an arbitrary number of slots with a single:subscribers[65535]write. Slot indices are stable per §D.2, so a slot that does not exist yet is not addressable.- Parameters:
idx – The
:subscribers[N]slot number.s – The replacing edge.
latch – Optional durability latch; snapshotted iff the REPLACING subscriber requested durability (RFC-0022 §3.A) and the vertex holds an LKV.
displaced_remote – Optional out-parameter receiving the displaced edge’s cold
remotehalf (#1816), moved out under the lock. A cleared slot holds none, so it stays empty unless a live remote edge was displaced.
- Returns:
Which case applied — see edge_replace_t.
-
inline std::size_t evict_link_edges(std::string_view link, std::size_t &routed)¶
Deactivate AND reclaim every active subscriber edge stored against the link
link— the per-vertex half of peer-departure eviction (RFC-0009 §D, extended to link teardown).Matches each active slot on the link it was ADMITTED over: the cold half’s
subscriber_remote_t::linkwhen it carries one, and otherwise itssubscriber_remote_t::caller— the two spellings the two admission doors leave behind for the SAME fact.subscribe_wire(theSUBSCRIBEop and the wire:subscribers[]append) stores both;graph_t::field_write’s:subscribers[]/:subscribers[N]arms store ONLY the context, because those edges deliver to a LOCAL target and have no return route to send over. Matchinglinkalone therefore left a field-write-admitted edge permanently un-evictable — active, counted, and still fanning out to its target under a gate context whose session had departed (#943). ADR-0018 defines that context as this node’s NAME for the inbound link a remoteFWDarrived on, i.e. the same name spacelinkis spelled in, so the fallback compares like with like. A local edge still never matches a real link name: a local door passes the EMPTY context and stores no cold half at all (the one exception,parse_subscriber_tlv’sdelivery_compactopt-in, leaves both spellings empty). An EMPTYlinkmatches nothing at all and returns 0 — a link with no name never subscribed, and without that rule the empty key compared equal to exactly those empty spellings and reclaimed every localdelivery_compactedge on the vertex (#1056). Unlike clear_edge, a matched slot is RECLAIMED, not just flagged: the stored SUBSCRIBER view, the return-route refcount pin, the target key, and the wholesubscriber_remote_tblock are released in place (the slot shell stays — §D.2 index stability — and add_edge reuses it). An in-flight delivery is unaffected: its edge_view_t snapshot HOLDS the target key and the wholesubscriber_remote_tby refcount (ADR-0041 §2, #1448), so releasing the slot’s pin here never dangles a dispatch — the record it reads outlives this eviction by construction.- Parameters:
routed – Incremented once per evicted edge that was ROUTED through
link— stored it as its delivery link rather than only as the gate context — which is the count of link holds the eviction gives back (#1816).- Returns:
The number of edges evicted (the caller unwinds exactly this many from the RFC-0005 listener bookkeeping).
-
inline std::size_t evict_route_edges(std::string_view link, std::span<const std::byte> route, bool bound_echo = false)¶
Reclaim the remote edges whose delivery
linkAND stored returnrouteboth match — the per-vertex half ofgraph_t::evict_route_edges(#1223 step 5).The narrow sibling of evict_link_edges — where that one reclaims EVERY edge a departed link admitted, this one reclaims exactly the edge(s) whose next hop refused the stored route with an addressed
tr::path::invalid(RFC-0020) — the one wire observation a producer gets about a route whose terminal session departed. Matchinglinkalone would evict every edge sharing the mount; matching the route alone would let any link speak for another’s edges — both keys are required, and the route compare is BYTE-equal on the stored PATH TLV (the same bytesdeliver_remoteemits as the deliverydst, which are the bytes the refusing hop echoes back — seereject_bus_name_hop’s swap). Onlysubscribe_wire-door edges qualify: a field-write edge stores no route, androutenever compares equal to its empty view. An EMPTYlinkorroutematches nothing, as in evict_link_edges (#1056).- Parameters:
link – This node’s NAME for the link the refusal arrived on (== the edge’s delivery link).
route – The refused route — the whole TLV bytes echoed by the rejecting hop: a canonical PATH, or (RFC-0024 §7.1 amendment 1) the bound
PATH_REFa reverse-list delivery was refused as.bound_echo – True ⇔
routeis thePATH_REFform — the caller classified the echo’s type byte (this header stays wire-type-agnostic), and the match runs against the stored reverse list’s emitted suffix instead of the canonical return route.
- Returns:
The number of edges evicted (the caller unwinds exactly this many from the RFC-0005 listener bookkeeping).
-
inline std::size_t snapshot_edges(edge_snapshot_t &inline_buf, std::vector<edge_view_t> &overflow, snapshot_drops_t &drops)¶
Snapshot every ACTIVE edge’s dispatch view into caller storage — the snapshot-under-pin half of the snapshot/dispatch-after-release discipline.
Small fan-out (the common case, ≤
kInlineFanout) placement-constructs intoinline_buf— no heap allocation AND no dead stack zeroing per publish; a larger subscriber list reservesoverflowonce and fills it instead (thenoverflowis non-empty and holds ALL views).The ONE way this can come back short is NOTHROW (#477 — this runs on the writer thread’s fan-out, where a bad_alloc is an abort() under
-fno-exceptions): an unreservableoverflowdegrades the snapshot to the firstkInlineFanoutviews ininline_buf, and the rest of this delivery is dropped. It is TALLIED intodrops(snapshot_drops_t) so the caller can report it; it is not silent (#896). The per-edge copy itself cannot fail at all since #1448 — it is two pointer copies and two refcounts on EVERY edge shape, remote included — so the whole snapshot reaches an allocator only for that one overflow reservation. NO LOCK (#635). The source is the vertex’s PUBLISHED, immutable-after-publish edge array, read under a bounded per-participant EDGE PIN (edge_pin.hpp) whose scope is this copy loop and nothing else — released before the caller’s firstdispatch_edge, so a subscriber callback that re-enters the graph always finds this thread’s cell empty (pin_tasserts it). What this deletes is the stripe mutex, which serialised the publishes of every vertex that merely HASHED to the same stripe: measured at ×16.6 with NEGATIVE scaling past four threads. What it does not add is any shared-cacheline RMW — the announcement is aseq_cststore to this thread’s own isolated cell, which is the whole reason a refcounted published array was rejected instead.Fallback: a thread that cannot claim a pin (more publishers than
kEdgePinSlots) copies the CURRENT array under the stripe mutex — safe because displacing an array requires that same lock. Correctness never depends on the constant; only scaling does.- Parameters:
inline_buf – The caller’s raw stack buffer (cleared on entry).
overflow – The heap fallback for large fan-out (cleared on entry).
drops – Out: what this snapshot SHED (snapshot_drops_t), zeroed on entry. By reference, not optional — a caller that may not see the shed count is the #896 defect itself.
- Returns:
The number of views snapshotted (into whichever buffer was used).
-
inline std::optional<view::view_t> edge_source(std::size_t idx)¶
The stored SUBSCRIBER TLV view of the active slot
idx(a:subscribers[N]read) — a refcount clone, no byte copy;nulloptfor a missing / inactive / TLV-less (in-process sugar) slot.
-
inline std::vector<view::view_t> edge_sources()¶
Every active slot’s stored SUBSCRIBER view, in slot order (the
:subscribers[]array read) — each a refcount clone.
-
inline void clear_declarations() noexcept¶
Take back everything a registration DECLARES, to an unregistered placeholder’s default: the policy members (retention by role with no
RETAIN_NONE, depth 1, no ring so no ring source and best-effort, the default share threshold, no app-field group) and the payload-right and admission flags.The flags belong to the occupant, not to the address (RFC-0014 Amendment 2): the graph’s rows and filter nodes for this vertex stay parked and unreachable while the bits are clear, and a re-registration that declares again prepends its own, newer node.
Retirement calls it, and so does a registration refused after its declarations landed on the placeholder (#1778), so a later registration through a door that brings no policy (the write-create
ensure_vertex) inherits none of it. The caller MUST hold the graph map lock; the stripe lock is taken here. Allocates nothing.
-
inline value_handlers_t *revert_to_placeholder(tr::mem::block_array_t<subscriber_t> &gone)¶
Restore this vertex to the state an unregistered PLACEHOLDER carries — the
unregistered ⇒ carries no stateinvariant retirement re-establishes (RFC-0009 §B.6). Clears everything a installs plus everything it leaves behind, so a later revive of this address inherits nothing of the retired owner: the value seam (swap-and-park, never freed — a lock-free reader may still hold the old pointer), the stored value and history, the:acl(own ACEs + the cached merge), the app-field table, the storage policy, the role, and the delivery mode. Survives by design:write_seq_(forward-only per address, mod 2^32; a reset would hand a liveawaitsnapshot back its own value),listeners_above_(counts ANCESTOR subscribers, which retiring THIS vertex never touched — the graph adjusts it for cleared descendant edges), and the allocation / name / links (ADR-0057 insert-only — emptied, never freed or detached).Note
registered_is NOT touched here — it is map-lock state the graph flips. The caller MUST hold the graph map lock. This RETURNS the swapped-out value-seam block (or nullptr) rather than freeing it: a lock-free reader may still hold the old pointer, so the graph parks it and the embedder frees the park throughgraph_t::collect()(#576). The per-vertex stripe lock is taken internally.- Parameters:
gone – Receives this vertex’s whole subscriber slot table (MOVED out, never copied, so it cannot fail): the graph gives each ROUTED edge’s link hold back and drops the table once its locks are released (#1816, #1778). Left as it was when this vertex has no edge block.
- Returns:
the detached seam block to park, or nullptr if this vertex had none.
-
inline bool set_acl(std::vector<ace_t> aces, tr::mem::block_source_t &tables)¶
Store this vertex’s
:aclas typed ACEs — the ONLY stored ACL state (#907).Storing replaces, and marks the ACL PRESENT: an empty list is the sanctioned clear-enforcement write (⇒ no restrictions) and still reads back as an empty ACL, not as the NOT_FOUND of a vertex that never had one. Takes no raw bytes, because there is no second copy to fall out of step with the list evaluation walks — an
:aclread re-encodes from here.
-
template<typename F>
inline auto with_acl(F &&f) -> decltype(f(false, std::declval<const std::vector<ace_t>&>()))¶ Run
fover this vertex’s whole:aclstate — the presence bit and the parsed ACE list, read together under ONE hold — the read-back accessor (#907).The caller re-encodes the list it is handed (
graph::encode_acllives a layer up and cannot be named from here), which is what makes an:aclread canonical: it serves a projection of the SAME listacl_allowsevaluates, so the two can no longer disagree. Presence and list travel together because a clear that landed between two accessors would otherwise be served as an ACL that no longer exists.fmust not re-enter this vertex — the lock is held.- Returns:
Whatever
freturns.
-
template<typename F>
inline auto with_aces(F &&f) -> decltype(f(std::declval<const std::vector<ace_t>&>()))¶ Run
fover this vertex’s parsed ACE list under the vertex lock — the zero-copy evaluation accessor (graph_t::acl_allowshands the list to the pure ADR-0050 policy without snapshotting subject bytes per gated op).fmust not re-enter this vertex (the lock is held) — it is a pure evaluation over the list, per the ADR-0050 policy contract (no locks/clock/graph inside).- Returns:
Whatever
freturns.
-
inline void mark_acl_cache_dirty() noexcept¶
Mark this vertex’s cached effective-ACE merge stale (ADR-0050/0078).
Raised by the graph on every
:aclwrite for the WRITTEN vertex’s whole subtree (subtree-precise invalidation via the ADR-0057 child links — wiring-frequency); set_acl raises it for the written vertex itself. The next with_effective_aces on a marked vertex rebuilds lazily.Note
Lock-free (one uncontended CAS) — callable under the graph’s map lock during the subtree walk without touching any vertex mutex.
-
template<typename Rebuild, typename Eval>
inline auto with_effective_aces(Rebuild &&rebuild, Eval &&eval) -> decltype(eval(std::declval<const std::vector<ace_t>&>()))¶ Evaluate against this vertex’s cached effective-ACE merge, rebuilding it first iff it is stale — the ADR-0050 cached-merge verb.
Staleness is ONE bit of ONE word (ADR-0078):
vertex_ext_t::acl_genis odd. When it is, that odd value and this vertex’s own parsed ACEs are SNAPSHOTTED andrebuildruns with the stripe lock RELEASED (#361 §2) — the graph’s rebuild walks the immutable parent chain taking each ancestor’s with_aces one stripe lock at a time, never nested, so an ancestor sharing this vertex’s stripe cannot self-deadlock. Back under the lock the rebuilder publishes by CAS-ing that snapshot to snapshot + 1 (even), thenevalruns.Race resolution (rebuild vs concurrent
:aclwrite): every invalidator — set_acl, the placeholder revert, the subtree mark an ancestor:aclwrite fans out (mark_acl_cache_dirty) — advances that one counter viainvalidate_acl_cacheafter publishing its ACEs, lock-free, and does NOTHING else. The recheck and the publish are therefore the SAME atomic operation, so an invalidation landing anywhere in the rebuild defeats the CAS. That is the whole of the coherence argument, and it is what the retired{acl_gen, acl_cache_dirty}pair could not give: there they were two ops, and a mark landing between them was overwritten bydirty = false, pinning a stale merge as clean FOREVER (#880) — a revoked policy still enforced. A failed CAS also discards amergedthat may be TORN across the write rather than answering from it. The one premise left is that the counter does not WRAP onto a stale-but-even value (vertex_ext_t::acl_gen).- Parameters:
rebuild –
std::vector<ace_t>(const std::vector<ace_t>& own)— the fresh merge over a snapshot of this vertex’s own ACEs; runs UNLOCKED (it may take other vertices’ stripes freely).eval – Pure evaluation over the cached merge. A BARE descendant evaluates the merge’s
kAceInheritsubsequence, whichevalselects witheffective_acl_t::allows’srequired_flagsrather than receiving a second, pre-projected list — filtering in place is order-identical and costs no storage. ADR-0050 policy contract: no locks/clock/graph inside.
- Returns:
Whatever
evalreturns.
-
inline bool set_app_fields(std::vector<app_field_t> table, tr::mem::block_source_t &tables)¶
Install (or replace) the field descriptor table — the OWNER naming the holes in the closed
ENOTTYdefault (RFC-0010 §A.2), one more store-verbatim verb on this seam (theset_aclpattern).Replacement takes effect atomically with respect to concurrent field operations on this vertex (one lock hold). An empty
tableuninstalls — the vertex reverts to the closed surface, including the pre-RFC synthesized:schemashape — and, on a vertex that never had an extension block, allocates nothing (#361 §1: a leaf with no app fields pays nothing).
-
inline bool set_app_fields_static(borrowed_fields_t table, tr::mem::block_source_t &tables)¶
Install a BORROWED descriptor table (ADR-0058): the slots view the caller’s
tablestorage directly — zero declaration RAM. The array AND thename/descriptorbytes it points at MUST outlive the vertex (static/flash storage); borrowed_fields_t is what constrains the argument’s shape to match. Declaration only; values are written later via the field-write surface. Same uninstall-on-empty and allocate-nothing-on-empty-leaf semantics as vertex_policy_t::app_fields.
-
inline std::optional<app_access_t> app_field_access(std::string_view name)¶
The declared access of the app field
name(nullopt⇒ undeclared — the graph’sSCHEMA_NOT_FOUND).
-
inline result_t<void> app_field_store(std::string_view name, std::span<const std::byte> bytes)¶
Store
bytesverbatim into the DECLARED app fieldname(RFC-0010 §D — bytes in, bytes out; no dtype/range validation, the descriptor is consumer self-description) — or store nothing, if the field retains nothing (wo, or declared retention_t::NONE; RFC-0028 §5.4). The caller’s apply seam fires either way.- Returns:
SCHEMA_NOT_FOUNDiffnameis not declared (e.g. a concurrent table replacement removed it between the caller’s gate and this store), andBACKPRESSUREwhen the table source could not hold the bytes (#1778) — the field keeps its previous bytes.
-
inline app_read_t app_field_get(std::string_view name, std::vector<std::byte> &out)¶
Read the app field
nameintoout(the stored TLV bytes, verbatim);outis written only on app_read_t::OK.
-
inline std::vector<app_field_t> app_fields_snapshot()¶
A consistent copy of the whole descriptor table, in install order — the container-read /
:schemasnapshot (control-plane cold; empty ⇒ no table installed).
-
inline bool set_retention(retention_t r, std::uint32_t depth, tr::mem::block_source_t &tables)¶
Declare what this vertex retains (RFC-0028 §5.4, D4) — owner-side, never over the wire. The role/retention pairing is
graph_t::set_retention’s to validate.retention_t::NONE sets the one flag bit the write path tests, and DROPS whatever is already held — the last-known value and every queued ring entry (their reservations back to the source that served them) — so
readanswersNOT_FOUNDfrom the call on. Zero bytes: the bit lives in the flag bytevertex_talready has.retention_t::LAST clears the bit.
retention_t::N clears the bit and records
depth, allocating the extension block if this vertex has none (a STREAM vertex always has one already), under the vertex mutex the ring append re-reads it under — so a depth change and a concurrent append cannot interleave halfway. The next append trims to it.
Wiring-time, like
set_ring_source— a store racing a switch toNONEmay land the one value the switch was meant to drop.- Parameters:
r – The retention.
depth – Entries to retain under retention_t::N; 0 is normalised to 1 by the ring trim. Ignored otherwise.
-
inline bool retains_none() const noexcept¶
True iff this value vertex was declared retention_t::NONE — the write path’s one relaxed test of the flag byte it already reads for admission. A
HANDLERretains nothing by role and does not carry the bit.
-
inline retention_t retention() const noexcept¶
What this vertex retains:
NONEfor aHANDLERor a vertex declared so,Nfor aSTREAM(its ring), elseLAST.
-
inline std::uint32_t retention_depth() const noexcept¶
The ring depth a
STREAMretains under retention_t::N (1 when never declared).
-
inline bool set_ring_source(tr::mem::block_source_t *src, bool reliable, tr::mem::block_source_t &tables)¶
Bind this RECEIVING vertex’s own ring source and §4.4 pressure arm — the seam of RFC-0025 §4.6.1 clause 3, owner-side and with no wire surface.
Sited on
vertex_ext_t’s lazily-allocated ring block, never onvertex_titself: a STREAM identity already allocates the extension block, and the whole of the ring’s state hangs off the one lazy pointer that block already held — sosizeof(vertex_t)does not move,sizeof(vertex_ext_t)does not move either, and a vertex that never receives pays nothing. The #1285 ratchet and the RAM census are both untouched.REBINDING DRAINS. Reservations are released to the source that served them (sized reclaim), so a rebind first hands every queued entry’s block back to the OLD source and empties the ring; the queue restarts on the new budget. Wiring-time by intent — the “configure before frames flow” contract
set_retentionandset_delivery_modealready carry.- Parameters:
src – The source this receiver’s admissions are charged against.
nullptrunbinds, so the next admission re-resolves the graph-level default.reliable – The §4.4 arm: false (default) best-effort — shed oldest, account the loss, raise a gap; true reliable — refuse the admission and answer the local producer
BACKPRESSURE, shedding nothing.
-
inline bool ring_reliable() const noexcept¶
This receiver’s §4.4 arm:
trueonce declared RELIABLE (see set_ring_source);falseby default.
-
inline std::span<const app_field_slot_t> app_field_slots() const¶
The installed app-field slots (empty when none) — what
graph_t::set_policycompares a borrowed declaration against, so re-applying it keeps the values.
-
inline tr::mem::block_source_t *ring_source() const noexcept¶
This receiver’s bound ring source, or
nullptrwhile it still draws the graph-level default (nothing admitted and nothing declared).
Set this vertex’s copy-or-share threshold (RFC-0028 §5.3) — owner-side, never over the wire.
0shares always;SIZE_MAX(or anything fromUINT32_MAXup) copies always.Published under the vertex mutex; the write-path reader (share_threshold_bytes) takes no lock, because a threshold changing under a concurrent write only decides WHICH correct store shape that write takes.
-
inline delivery_mode_t delivery_mode() const noexcept¶
How this vertex participates in an ANCESTOR’s propagate sweep (RFC-0008 §C).
Relaxed, and deliberately racy against a concurrent
set_delivery_mode: the assign path reads it lock-free as a FAST PATH only (graph_t::mark_pending), and whichever of the two values it observes there, the decision that actually places the vertex in a sweep set is re-taken under the graph’s sweep lock. ATOMIC becauseset_delivery_modemay run concurrently on another thread (#895) while this read holds NO lock — which is the whole reason it needs to be atomic, and what distinguishes it from the other plain members of the same byte group:registered_is map-lock state on both sides (seemark_unregistered), so a plainboolis correct there.
-
inline void set_delivery_mode(delivery_mode_t mode) noexcept¶
Set the propagation policy — wiring-time, via
graph_t::set_delivery_mode(which also maintains the sweep’s UNCONDITIONAL membership, and holds its sweep lock across this store so the two stay one decision).
-
inline bool has_own_aces() const noexcept¶
True iff this vertex has its OWN parsed ACEs (#361 §3) — the lock-free predicate of the graph’s nearest-bearing-ancestor walk. Relaxed read: a racing
:aclwrite is observed by the next gated op at worst, the same window the dirty-flag protocol already tolerates.
-
inline std::uint32_t own_subs() const noexcept¶
This vertex’s own active-slot count (what a subtree walk sums).
-
inline std::uint32_t own_subs_ordered() const noexcept¶
The same count under
seq_cst— the SUBSCRIBE half of a Dekker pair, and the only read that may be used to SKIP a DELIVERY (#635, #1140).A relaxed read is fine for every consumer that only decides how much work to do (own_subs above). It is NOT fine for one that decides whether to deliver at all: a publisher that skips
snapshot_edgeson a zero count must be ordered against a subscribe that is concurrently taking ADR-0049’s durability latch, or the new subscriber gets the latch’s OLD value and never sees the publish that raced it.“Skip a delivery” covers both halves of the write path, EAGER and DEFERRED. #635 fixed the eager one (
graph_t::fan_out’s snapshot skip); #1140 fixed the deferred one (graph_t::mark_pending, where a skipped mark leaves the vertex in no sweep set, so the next coveringpropagatedelivers it nowhere). The distinction between skipping a fan-out and skipping a mark is bookkeeping — the lost delivery is the same, so the same read is required. Only the OWN half; the ancestor count keeps its relaxed load, see listeners_above.The pairing is the one
storealready documents forwaiters. PUBLISHER: store the LKV, THEN load this count. SUBSCRIBER: bump this count, THEN load the LKV into the latch. Both sidesseq_cst, so they share one total order: a publisher that reads zero is ordered before the subscriber’s bump, hence before the subscriber’s latch load — so the latch carries the value the skipped fan-out would have delivered. The other interleaving (count already bumped, slot not yet appended) costs one pointless lock acquisition that snapshots nothing, never a lost delivery.Both halves take
kDeliverySkipOrder, which every buildstatic_asserts is stillseq_cst— so the argument above is a build failure when it stops holding, not only a paragraph (#1143).
-
inline void bump_own_subs(std::int32_t delta) noexcept¶
Adjust the own active-slot count by
delta(subscribe/unsubscribe).Note
seq_cst, not relaxed: this is the subscriber’s half of the pair own_subs_ordered describes. Subscribe is control-plane-cold, so the stronger order costs nothing that is measured.
-
inline std::uint32_t listeners_above() const noexcept¶
The active subscriber slots on strict ancestors — the one relaxed load the write hot path pays before deciding whether to walk ancestors at all.
Note
Relaxed BY RULING even where it gates a SKIP, so there is no
_orderedtwin (#854, measured and REFUTED): a stale zero here is indistinguishable from the write linearizing before the racing subtree subscribe, because ADR-0049’s latch snapshots the subscribed ANCESTOR’s own LKV (add_edge) and never a descendant’s — so unlike own_subs_ordered’s near-axis pair there is no forbidden observation to exclude, and theseq_cstcandidate doubled the idle write’s rv32 fence count to exclude nothing.
-
inline void bump_listeners_above(std::int32_t delta) noexcept¶
Adjust the ancestor-listener count by
delta(an ancestor’s edge came/went).
-
inline void init_listeners_above(std::uint32_t count) noexcept¶
Seed the ancestor-listener count at creation (the newborn’s O(depth) sum).
Public Static Functions
-
static inline bool disarm_waiter(await_waiter_t &w) noexcept¶
Unlink
wif it has not fired yet (a timeout or a cancel).Needs no live vertex: the stripe is derived from the address
wrecorded.- Return values:
true –
wwas still armed and is now the caller’s; the waiter’s fire callback will never be called for it.false – A publish already took it: its fire runs (or ran) on the writer’s thread.
Public Static Attributes
-
static constexpr std::size_t kInlineFanout = edge_snapshot_t::kCapacity¶
The no-heap small-fan-out snapshot width (snapshot_edges buffer size).
-
static constexpr std::size_t kNoSlot = static_cast<std::size_t>(-1)¶
What add_edge answers when the edge could NOT be admitted — the injected resource is exhausted (#477: the writer soft-fails by value; a
bad_allocunder-fno-exceptionswould be anabort(), and admission is reachable from a peer’s bytes since RFC-0014).A caller that sees it must NOT count a listener: nothing was appended and nothing was published, so the vertex is exactly as it was.
-
class ring_take_t¶
The STACK-FIRST buffer a STREAM ring’s unflushed window is taken into (#1713): the first kInline entries live in the caller’s frame, a wider window spills once to the heap.
The write path fills it in the SAME stripe-lock section that admits the entry (
ring_admit’s take out-param), and the sweep path through take_unflushed, so a STREAM write is one lock section and — in the common case, where the window is the write’s own entry — no allocation at all. The heapstd::vectorit replaces cost a malloc/free per write on a host, and two pairs on a-fno-exceptionstarget, where the nothrow reserve probes before it commits.It is transient caller storage, never a library-held buffer: it lives for one delivery and holds refcount shares of entries the ring already owns. The spill keeps the old drain’s contract exactly — a window that cannot be snapshotted is NOT taken, so the cursor stays put and the next covering flush re-takes it (deferred, never lost, #477).
Public Functions
-
inline explicit ring_take_t(tr::mem::block_source_t &src) noexcept¶
An empty take whose spill, if one is ever needed, draws from
src(the graph’s value source, #1778); nothing allocated.
-
ring_take_t(const ring_take_t&) = delete¶
Non-copyable — transient delivery storage, never a value.
-
ring_take_t &operator=(const ring_take_t&) = delete¶
Non-assignable — transient delivery storage, never a value.
-
inline std::span<const value_ref_t> entries() const noexcept¶
The taken entries, oldest first.
-
inline bool engaged() const noexcept¶
Did a ring admission run the take at all? False only when the store never reached a STREAM ring (a role that changed under a racing retire), which is the one case the caller falls back to a separate drain for.
Public Static Attributes
-
static constexpr std::size_t kInline = 4¶
The in-frame width: 4 entries — 32 B on a 64-bit host, 16 B on rv32.
A STREAM write’s window is its OWN entry (the fused take empties the cursor every write, so a concurrent writer takes its own entries too); a wider window only comes from
assigns queued without a flush. Four covers a write behind a short burst of those, and the slots cost less than oneedge_view_tof frame. A strategy knob, not a limit: a wider window spills and is delivered whole (STYLE.md, counting doctrine 6).
-
inline explicit ring_take_t(tr::mem::block_source_t &src) noexcept¶
-
struct snapshot_drops_t¶
What a snapshot DECLINED to hand back: the deliveries a vertex shed before the graph could dispatch them (#896).
snapshot_edgesis allowed to come back short, and the way it can is a specified drop rather than an abort (#477). A drop nobody counts, though, is indistinguishable from a delivery that never had to happen — which is how a whole fan-out could be shed under memory pressure whilegraph_t::delivery_drops(), the one observable, read zero.vertex_towns no counters (it is the storage layer, not the instrumentation layer): it reports the tally by reference andgraph_t::fan_outfolds it into the graph’s per-cause counters at the frame that owns them.There used to be a second cause, and #1448 deleted the failure, not the report. A per-edge
out_of_memorycounted the edges whose owning link / caller copies could not be allocated. edge_view_t no longer copies them — it takes a refcount share of the immutable cold half — so the per-edge snapshot reaches no allocator on any edge shape and cannot fail. What remains is the capacity degrade: a fan-out wider than the inline snapshot, on a heap that would not lend it a buffer. The OUT_OF_MEMORY delivery cause is untouched and still counted from the legs that can still hit it (graph_t::dispatch_edge_target’s store — a declined slot, ring or clone).Public Functions
-
inline bool any() const noexcept¶
Did this snapshot shed anything? The ONE test a clean fan-out pays.
Public Members
-
std::uint32_t truncated = 0¶
Edges past the inline prefix, abandoned because the overflow buffer for a wide fan-out could not be reserved — the capacity degrade.
-
inline bool any() const noexcept¶
-
struct store_drops_t¶
What a
storeSHED under allocation pressure — reported BY REFERENCE, never counted here (#1003).The same division of labour snapshot_drops_t states for the fan-out plane, for the same reason:
vertex_tis the storage layer and owns no counters, so it reports the tally andgraph_tfolds it through the single exhaustive counting door. A shed the storage layer knows about and the graph never hears of is exactly the defect — a whole STREAM fan-out was abandoned under memory pressure whilegraph_t::delivery_drops(), the one observable, read zero.The width is the CALLER’s call, not this struct’s: whether a shed append cost a delivery depends on whether the ring drain was the delivery (it is for the write and sweep paths; it is not for a branch notify, which fans the slice out eagerly and then flushes the cursor). See
graph_t::count_store_drops.Public Functions
-
inline bool any() const noexcept¶
Did this store shed anything? The ONE test a clean write pays.
Public Members
-
bool ring_append = false¶
The RECEIVER’s STREAM ring could not admit: its injected source declined the reservation and the ring had nothing left to shed, so the entry never entered the ring. The LKV publish ABOVE it still landed — the write succeeds (RFC-0008 §E, bounded-lossy history), and what is lost is the delivery a later drain would have made.
-
std::uint64_t ring_shed = 0¶
How many queued entries the best-effort arm SHED to make room (RFC-0025 §4.4: “shed the oldest, whole, never partial”). Each one is both a lost delivery and a
tr::flow::address_shift_gappoint; silence here is the one behaviour the RFC forbids.
-
inline bool any() const noexcept¶
-
enum class edge_replace_t¶
-
enum class tr::graph::emission_mode_t : std::uint8_t¶
How a
graph_t::propagatesweep EMITS what it selected (RFC-0025 §4.1.2, Amendment 3 clause 5) — a producer-side choice about framing, never a subscription negotiation.Orthogonal to
delivery_mode_t— that mode decides WHICH vertices a sweep selects, this one decides how the selection reaches the wire. Neither is a per-subscriber knob, and neither is readable or writable by a peer — the producer owns cadence and framing (RFC-0005 §Motivation-3, RFC-0025 §3).Values:
-
enumerator PER_VERTEX¶
The DEFAULT, unchanged: one
FWD{WRITE}per selected vertex (RFC-0008 §D).
-
enumerator FOLD¶
ONE branch-write frame for the swept subtree — the RFC-0016
POINTtree, node shape byte-for-byte RFC-0005 §B’s, root carrying its leadingNAME. One frame per SUBTREE and never a container across several (the retired-LIST ban, RFC-0005 §E).
-
enumerator PER_VERTEX¶
-
struct edge_block_t¶
A vertex’s edge state, allocated on FIRST subscribe and freed with the vertex.
Pay-for-what-you-use, the #361 §1 discipline: an edgeless vertex — the overwhelming majority on an MCU node — owns a single null pointer, where it used to own an empty
std::vector(24 B on a host, 12 on rv32). The block itself is never displaced or reclaimed, only its published arrays are, which is whysnapshot_edgesmay load it with a plain acquire and no pin at all.slots is the MASTER: the
:subscribers[N]slot table, index-stable per RFC-0009 §D.2, mutated only under the vertex stripe mutex exactly as it was before #635. pub is the dispatch-side projection of it that publishers read without any lock.Public Functions
-
inline explicit edge_block_t(mem::block_source_t &src) noexcept¶
An empty block drawing from
src.
-
inline ~edge_block_t()¶
The teardown flush: free the published array AND everything still parked.
The contract this states, in the same shape ADR-0069 §6 states for the LKV domain: the graph — and therefore every vertex — must OUTLIVE the threads that published through it. A thread still inside
snapshot_edgeswhen its vertex is destroyed is a use-after-free with or without this mechanism, and the ASan/LSan legs exercise the flush on the joined-threads side of that line.
Public Members
-
mem::block_array_t<subscriber_t> slots¶
The master slot table (stripe-locked). Its source served this block and serves every published array too (#1778).
-
std::atomic<edge_pub_t*> pub = {nullptr}¶
The published array (null ⇒ no edges).
-
std::atomic<edge_pub_t*> retired = {nullptr}¶
Displaced arrays awaiting a scan.
-
inline explicit edge_block_t(mem::block_source_t &src) noexcept¶
The receiver-side STREAM ring (RFC-0025 §4.6.1): one lazily-allocated block per receiving
vertex, holding the queued entries, the block_source_t their admissions are charged against,
the §4.4 pressure arm and the gap census. Each entry carries the reservation it was admitted
under — admission, not placement: the payload stays where the publish put it.
-
struct ring_state_t¶
A receiving vertex’s whole STREAM-ring state, LAZILY allocated as one block (RFC-0025 §4.6.1 clause 3) — the entries, the injected source they are charged against, the pressure arm, and the gap census.
Grouped behind ONE pointer on purpose, and that is the difference between a seam every ext-bearing vertex pays for and one only the receivers do.
vertex_ext_talready held a lazy pointer for the ring’s entries; hanging the source, the arm and the two counters off that same pointer keepssizeof(vertex_ext_t)EXACTLY where it was, so a vertex with app fields, an:aclor a handler — which allocates the cold block for reasons of its own and will never admit a stream entry — pays zero additional bytes. The RAM census (vertex_app5,vertex_app5_static,reg_escape) is the gate that says so, and it caught the four-loose-members spelling of this at +32 B.The entries are an intrusive list of their own reservations (ring_entry_t): a push or a pop is a pointer swap, and the ring allocates nothing beyond what it charges.
Public Functions
-
inline void push_back(ring_entry_t *n) noexcept¶
Link
nin as the newest entry.
-
inline ring_entry_t *pop_front() noexcept¶
Unlink and return the oldest entry (the ring must not be empty). Its block is still reserved: the caller releases or reuses it.
-
inline void release_all() noexcept¶
Release every held reservation and empty the ring — the ONE place the charge/release pairing is closed, shared by the destructor, the placeholder revert and
graph_t::set_ring_source’s rebind. Idempotent. A non-empty ring always has a bound source: an entry exists only once a source served it.
-
inline ~ring_state_t()¶
Hand every reservation back before the block dies. Dropping the list without releasing them would leak the whole ring’s byte budget on every teardown.
Public Members
-
ring_entry_t *head = nullptr¶
The oldest queued entry, or null when the ring is empty.
-
ring_entry_t *tail = nullptr¶
The newest queued entry, or null when the ring is empty.
-
std::size_t count = 0¶
How many entries are queued.
-
tr::mem::block_source_t *source = nullptr¶
This receiver’s OWN injected source — the seam admissions are charged against.
Null until the first admission or an explicit `graph_t::set_ring_source`, at which point the graph-level default (itself defaulting to `tr::mem::heap_source()`) is BOUND here and stays bound: the destructor and every trim release against this exact source, and a sized reclaim cannot be served by a source that did not hand the block out. Per-injection-point, never a shared pool — ADR-0079's amendment measured a folded source collapsing to 0.01x of its own single-thread rate at T=24.
-
std::uint64_t gaps = 0¶
Shed points on this ring since it was created — each one a
tr::flow::address_shift_gap(RFC-0025 §4.5: “a detected discontinuity in an
ordered flow”), surfaced to the consumer IN ORDER through
vertex_t::drain_unflushed’s gap out-param and kept here for the census.
-
std::uint64_t gaps_drained = 0¶
How much of gaps a consumer has already been told about, so
vertex_t::drain_unflushedreports each shed point EXACTLY ONCE, in order, at the drain that follows it.
-
bool reliable = false¶
The §4.4 pressure arm this receiver binds under:
false(the default) is BEST-EFFORT — a refused admission sheds the oldest entry whole, accounts the loss and raises a gap;trueis RELIABLE — the admission is refused outright and the local producer is answeredBACKPRESSURE, with nothing shed and no growth past the byte bound. Declared owner-side throughgraph_t::set_ring_source; it is NOT a new knob on the wire. This declaration IS the ruled selector: RFC-0025’s 2026-08-24 §4.4 selector erratum (#1204) makes the receiving vertex’s own arm the one §4.4 binds, and demotes the subscription’sreliabilitybits to carried-verbatim-read-by-nothing. Receiver-pays (Amendment 2, §4.6.1): the party that funds the ring’s bytes declares what its overflow means.
-
inline void push_back(ring_entry_t *n) noexcept¶
-
struct ring_entry_t¶
One entry of a receiving vertex’s STREAM ring: the value, LIVING IN the RESERVATION it was admitted under (RFC-0025 §4.6.1 clause 3).
The reservation is the whole point, and the thing most easily misread. Admission calls
tr::mem::block_source_t::try_alloc(retained_bytes)on the RECEIVING vertex’s own source and holds the block until the entry retires (trim, drain-past, revert, destruction), at which point it is released. That bounds admission, in bytes, against a budget the receiver injected.The entry is placed at the FRONT of that block (RFC-0028 slice 6): the ring is an intrusive doubly-linked list of its own reservations, so the queue’s bookkeeping draws from the same injected source the byte bound charges and from nowhere else. The
std::dequethis replaced put its ~512 B map node and its chunks on the global heap, where no injected source ever saw them. kRingEntryOverhead issizeofthis struct, so every reservation is wide enough to hold it.It does NOT bound PLACEMENT of the payload. The payload never moves: value stays exactly the
value_tblock the publish minted, its links in whatever backend gave them, so the zero-copy handoff is preserved and a ring append is still a refcount bump. Physical placement migration is the later #873 family, explicitly out of scope here. A reader who assumes the ring’s payload bytes physically move into the injected source will be wrong, and the wrongness is expensive.Public Members
-
value_ref_t value¶
The published value — a refcount share of the LKV, never a byte copy.
-
std::size_t bytes = 0¶
The reserved width, as passed to
try_alloc— required to release the block this entry lives in (the sized-reclaim contract).
-
ring_entry_t *prev = nullptr¶
The next-older entry, or null at the head (the oldest).
-
ring_entry_t *next = nullptr¶
The next-newer entry, or null at the tail (the newest).
-
bool gap_before = false¶
True iff a shed happened immediately BEFORE this entry: the in-order
tr::flow::address_shift_gapmarker of RFC-0025 §4.4/§4.5, so a consumer draining the ring learns where the discontinuity is, not merely that one happened.
Public Static Attributes
-
static constexpr std::size_t kAlign = alignof(std::max_align_t)¶
The alignment every reservation is taken and released at.
-
value_ref_t value¶
-
enum class tr::graph::role_t : std::uint8_t¶
No target may weaken the delivery-skip pair (#1143, #1717).
The precondition #1140 could previously only state in prose, now compiler-checked: on a target that reorders a later relaxed load ahead of an earlier
seq_cststore — every shipped MCU target, and any many-core aarch64 host — the skip gate’s two halves must share one total order, which nothing weaker thanseq_cstgives them. AblatingkDeliverySkipOrderfails the BUILD instead of passing the suite on a TSO host and shipping a lost delivery to the targets CI cannot run.The assertion is unconditional. Until #1717 a build could set
kWeaklyOrdered = falseto claim a TSO target and waive it; nothing in-tree did, and the order isseq_csteverywhere, so the waiver bought nothing a build could use and the trait was removed.A vertex’s behavioral role (docs/reference/11 §roles). Byte-wide: it packs into
vertex_t’s flag byte group (#361 diet — 3 values need no int).Values:
-
enumerator STORED_VALUE¶
Role 1: last-writer-wins; holds the last-written value.
-
enumerator STREAM¶
Role 2: the CONSUMER’s bounded history ring — a queue the RECEIVING vertex owns (RFC-0025 §4.6.1 Amendment 2: “a producer
never queues”), bounded in BYTES by that vertex’s own injected
tr::mem::block_source_tand retained to a depth declared owner-side asretention_t::Nbygraph_t::set_retention(RFC-0028 §5.4; RFC-0022 §3.C).
-
enumerator HANDLER¶
Roles 3-7: user
on_read/on_writesupplies the behavior.
-
enumerator STORED_VALUE¶
-
enum class tr::graph::retention_t : std::uint8_t¶
What a vertex — or one of its application fields — RETAINS after a write is delivered (RFC-0028 §5.4, D4): one property with one spelling, where there used to be three (a role rule, a depth verb, and a separate store for
wofields).| | app fieldholder
default
legal
HANDLERvertexNONENONE— the handler consumes the valueSTORED_VALUEvertexLASTNONE,LASTSTREAMvertexN(depth 1)NONE,Nwith a depthapp field
ro/rwLASTNONE,LAST(Nreads asLAST— a field holds one value)wo|NONE|NONE— a write-only field has no read surface, so it stores nothing |NONEon a value vertex is the pure-relay shape: the write is delivered to every subscriber and released, the write sequence still moves (soawaitwakes), andreadanswersNOT_FOUND. It is a permitted policy, not a named role (RFC-0028 §11 ruling 3). Owner-side and host-only: no peer reads or writes it.Values:
-
enumerator NONE¶
Deliver and release; keep nothing (
read⇒NOT_FOUND).
-
enumerator LAST¶
Keep the last-known value — one slot, displaced by the next write.
-
enumerator N¶
Keep the last N entries in the receiving vertex’s ring (RFC-0025 §4.6.1): the depth is the INTENT, the ring source’s bytes the BOUND.
-
enumerator NONE¶
The node-scoped vertex index¶
graph_t keeps one dense, append-only vertex_t* slot per vertex ever allocated, in
allocation order, with the structural root at slot 0. It exists so a bound path’s u32
index means something: the vertex tree is a Composite of non-moving unique_ptr
allocations with no dense index of its own, and an element that named a tree position
would have to be a path again.
It costs 4 bytes per vertex on rv32, 8 on a host — the pointer and nothing else. The index is stored chunked rather than as one growing array for exactly that reason: a geometrically-growing array holds up to twice the pointers it needs between doublings, which measured 15 B per vertex on the 512-vertex heap probe against the 8 B the cost model charges. Fixed blocks make live bytes track the vertex count instead of the last doubling, and indexing stays O(1) with elements that never move. It is not a route table — its size tracks the graph, not the traffic — and it introduces no new lifetime rule, because registration was already insert-only. A slot is appended per allocation, not per registration, which is what keeps the mapping a bijection: retirement revives a vertex by filling the same object again, and a per-registration slot would give that object two indices depending on which side of the revive a mint fell.
deref_vertex_slot is the hot side and is the whole of the check — a bounds compare and a
generation compare, both under one shared map hold. It authorizes nothing; the operation
that follows re-evaluates the ACL at the vertex it returns.
The generation compare is bound_generation_matches, and it refuses a saturated
element outright rather than comparing it. Below the ceiling, “generations only move
forward” is the whole guard — a stale element compares lower and can never come back. At
the ceiling the counter stops, so a saturated element would keep matching its slot through
every subsequent retire and revive, with staleness detection permanently dead for that
slot. “Permanently unbindable” therefore has to be enforced on the side that honours an
element, not only on the side that issues one.
vertex_slot_at is the same read the other way round — index in, generation out, in O(1) —
and it exists for the FORWARDER’s mint: a hop mints for the connection vertex of the link a
reply arrived on, an index it recorded once at registration, so paying a scan of the whole
index per forwarded reply to re-derive an index it already holds would be the wrong shape in
the wrong place. It refuses a saturated slot exactly as the scanning form does.
allows(vertex, caller, right) publishes the ACL predicate every data op already runs, for
the one caller that reaches a vertex without performing a data op on it: the bound-path
forwarder, whose element dereferences to a connection vertex it will egress through
rather than read or write. Nothing is cached, so a revoked right takes effect on the very
next frame over an already-minted binding.
vertex_slot is the mint side. It returns the index and the generation together, from
one lock hold, because either alone is not a reference: read as two calls they can straddle
a retire, and the pair would then name the successor tenant’s vertex while the caller
believes it bound the one its operation reached. And it scans. That is deliberate rather than pending: a
per-vertex index field costs 4 bytes on rv32, where sizeof(vertex_t) sits at
config_t::kMaxVertexBytes32 with zero headroom, and a pointer→index side map costs
strictly more than the 4 B/vertex the slot vector does. A mint happens once per binding, on
a reply already being assembled.
Registration and subscription¶
-
class vertex_handle_t¶
A non-owning, non-null, opaque handle to a graph vertex (ADR-0056).
The caller-held result of graph_t::register_vertex / graph_t::find and the token handed back into every
graph_tdata op (read / write / await / assign / propagate / subscribe / history / field-write). Pointer-sized and trivially copyable, so it loads and passes exactly like thevertex_t*it replaces — identical codegen — but it exposes nooperator*or raw-pointer accessor: avertex_tis opaque L4 state, never dereferenced by callers. Constructed ONLY by graph_t (thefriend), which owns the pinned, pointer-stable, insert-only vertex map — so a handle always names a live vertex for the graph’s lifetime. There is no invalid/null state; “no such vertex” is modelled by thestd::optional<vertex_handle_t>graph_t::find returns.Friends
-
inline friend bool operator==(vertex_handle_t a, vertex_handle_t b) noexcept¶
Two handles compare equal iff they name the same vertex. (
!=is synthesized.)
-
inline friend bool operator==(vertex_handle_t a, vertex_handle_t b) noexcept¶
-
class subscription_t¶
An opaque handle to ONE in-process subscription — the token graph_t::unsubscribe removes it by (ADR-0049 host-SDK sugar for the wire
:subscribers[N]clear).Returned by the callback-form graph_t::subscribe overloads. It names a producer vertex and one of that vertex’s
:subscribers[]slots; the vertex is pinned for the graph’s lifetime (ADR-0057 — vertices are never freed), so the handle stays valid until it is unsubscribed. Trivially copyable and pointer-sized-plus-index — pass it by value.Opaque the same way vertex_handle_t is, and for the same reason (ADR-0056): the pair it carries is
graph_t’s state, not the caller’s.graph_tis the solefriend— the only code that can build one from a vertex and a slot, and the only code that can read either back — so a caller can neither reach thevertex_tbehind a live subscription (whose slot mutators are only valid under the graph’s locks) nor forge a handle from an arbitrary pointer and index and hand it to graph_t::unsubscribe. A default-constructed handle names no subscription and unsubscribes to aNOT_FOUNDno-op; operator== is the only observation a caller has.The reclamation guarantee this handle carries (ADR-0080)¶
unsubscribe()retires the edge, but the fan-out path snapshots a vertex’s edges and dispatches OUTSIDE every lock, so a snapshot taken before the retirement still names the subscriber’s{fn, callback_ctx}pair — the one leg of anedge_view_tsnapshot the library does not own a copy of. WHEN that pair becomes safe to free is therefore a real question, and ADR-0080 answers it with a build-time-closed, per-target policy (tr::graph::default_config_t::reclaim_policy_t), not with a runtime contract asking the caller to reason about in-flight state.This build’s guarantee is the one stated on the bound policy — tr::graph::reclaim_local_t (the default), tr::graph::reclaim_strict_t or tr::graph::reclaim_qsbr_t. Under all three the library owns the tracking and SIGNALS release through the tr::graph::subscriber_release_fn_t hook of graph_t::unsubscribe(const subscription_t&, subscriber_release_fn_t): the hook runs exactly once, outside every graph lock, at that policy’s grace point. There is nothing to poll and nothing to wait on — that shape is precisely what ADR-0080 §Decision 4 rejects.
The two per-thread policies state their guarantee over ONE thread’s dispatch domain, which is the single-threaded WIDE / MCU target they are for, and run the hook on the caller’s own thread. An embedder that dispatches from several threads at once and unsubscribes from another needs a grace period spanning every thread: bind tr::graph::reclaim_qsbr_t. Its one API difference — the hook may then run on a thread other than the
unsubscribe()caller, because a cross-thread grace period cannot promise otherwise without blocking — is stated on the policy itself.Public Functions
-
subscription_t() = default¶
A handle naming no subscription — graph_t::unsubscribe answers
NOT_FOUND.
Friends
-
inline friend bool operator==(const subscription_t &a, const subscription_t &b) noexcept¶
Two handles compare equal iff they name the same slot on the same producer vertex. (
!=is synthesized.)
-
subscription_t() = default¶
-
struct subscriber_t¶
One subscription edge (M3b).
A write to the owning vertex fans out to a target vertex (target_key — spec-faithful re-dispatch) and/or an in-process callback (sugar), per docs/reference/02 §dispatch + 04 §write fanout. An inactive slot models an unsubscribe (a cleared
:subscribers[N]). The wire/gate members live in the lazily-allocated remote half (#380 §3), so the plain in-process edge costs 80 B, not 160.Public Functions
-
subscriber_t() = default¶
A blank edge (an inert slot shell, or a door’s scratch record).
-
subscriber_t(const subscriber_t&) = delete¶
NOT copyable, exactly as it was while the cold half was a
std::unique_ptr(#380 §3). The handle that replaced it IS copyable — that is the point — so the ban is stated rather than inherited: a copied slot would share a cold half that ensure_remote is then entitled to write.
-
subscriber_t &operator=(const subscriber_t&) = delete¶
NOT copy-assignable — see the copy constructor.
-
subscriber_t(subscriber_t&&) = default¶
Movable: what the slot verbs do (append, reuse, reclaim-in-place).
-
subscriber_t &operator=(subscriber_t&&) = default¶
Move-assignable:
subs[idx] = std::move(...)is the reclaim.
-
~subscriber_t() = default¶
Releases this slot’s reference to the shared cold half.
-
inline subscriber_remote_t &ensure_remote()¶
The cold half, allocated on first use (admission-time only — never on a dispatch path), and MUTABLE only because the caller is still its sole holder.
The build phase of build-then-freeze (#1442). Every in-tree door fills a stack-local subscriber_t and only then hands it to
vertex_t::add_edge/replace_edge, so nothing has cloned the handle yet; the assertion states that rather than trusting it, because a write reached after a publish would mutate bytes a pinned reader may be copying under no lock.
Public Members
-
target_key_t target_key¶
Canonical PATH key (null ⇒ callback-only).
-
target_binding_t binding = {}¶
Minted slot for target_key (#830).
-
subscriber_fn_t callback = nullptr¶
In-process sink fn; null ⇒ target-only (ADR-0053 §6 rope value).
-
void *callback_ctx = nullptr¶
Caller-owned context passed back to callback; must outlive every delivery.
-
view::view_t source_view = {}¶
The original SUBSCRIBER TLV view this slot was written from, retained zero-copy (a refcount clone of the field-write payload).
Empty for in-process callback sugar that carries no TLV (the local target sugar DOES carry one — ADR-0049 encodes through the field-write door). A
:subscribers[]read ropes these slot views into theFWD{REPLY}with no byte copy (RFC-0004 §D / ADR-0035 slice 2 zero-copy reply rule). Stays HOT (outside remote) precisely because local field-write-door edges carry it.
-
remote_ptr_t remote¶
The cold wire/gate half (#380 §3) — null for the plain in-process edge; allocated by ensure_remote when a route/link/caller/compact-flag is stored (pay-for-what-you-use, ADR-0021). SHARED with every published entry that names this slot (#1442), never copied into one.
-
delivery_policy_t policy = {}¶
This subscription’s DELIVERY policy (RFC-0022 §3.A) — the packed 16 bits its
SUBSCRIBER.SETTINGS{ NAME "delivery_policy" }carried, or all-zero when it carried none. HOT, not in the coldremotehalf:durability_requestis read under the same lock hold that appends the slot, and it rides free in the padding beside active.
-
bool active = true¶
Active flag; an active edge receives every propagated value (delivery is value-agnostic — WHICH vertices a sweep propagates is the vertex’s
delivery_mode_t, never a per-subscriber byte comparison).
-
subscriber_t() = default¶
-
struct subscriber_remote_t¶
The COLD wire/gate half of a subscription edge (#380 §3), lazily allocated and refcount-shared, immutable after admission (#1442): the in-process edge — the common MCU wiring shape (callback or local target, empty caller) — keeps
subscriber_t::remotenull and pays one pointer instead of ~90 B of route/link/ caller state per edge.Build then freeze. An admission door fills one of these through subscriber_t::ensure_remote while the record is still private to its stack-local
subscriber_t; the slot verb then moves it in, and from the firstvertex_t::try_publish_edgesonward the record is READ-ONLY. Nothing in the tree writes it after admission —index_link_vertex’s key choice,evict_link_edges’ link compare,evict_route_edges’ route compare,edge_view_ofand the dispatch snapshot (vertex_t::copy_published, #1448) are all reads — which is what makes sharing it correct rather than merely cheap.That immutability is the whole fix for #1442. Before it, a republish DEEP-COPIED this record into a fresh
pub_remote_tper pre-existing entry, per admission — one nothrowoperator newplus up to twostd::stringheap copies each — andscan_retired_edgesfreed them all again on the next pass. Measured at ~940 instructions per pre-existing edge against a ~158 inherent floor at 65 links (bench/README.md, Where the whole-subscribe growth goes), i.e. ~83 % of the constant spent reproducing bytes byte-identical to the ones being retired. A republish now copies a pointer and increments refs.Public Members
-
std::string link¶
This node’s NAME for the link the subscribe arrived on.
FIRST, and the member ORDER below is the retired
pub_remote_t’s, not this record’s historical one. That is deliberate and load-bearing: unifying the two halves means the DELIVERY path (graph_t::dispatch_edge_remotesince #1448;vertex_t’s published-entry copy before it) reads this record instead of a published copy, and keeping the offsets it reads at exactly where they were is what kept that loop’s instruction stream identical across #1442. The slot-side readers (edge_view_of,evict_link_edges,evict_route_edges) move their displacements instead — control-plane paths, none of them pinned.
-
view::view_t return_route = {}¶
The consumer’s accumulated return route (a complete PATH TLV’s bytes — the FWD
srcthe subscribe arrived with).A write hands (link, this route, delivery_compact, value) to the graph’s injected remote-delivery sink, which emits the
FWD{WRITE}(or auto-promoted COMPACT) back over the link (RFC-0004 §D/§E.1, ADR-0035 slice 4 / #136).link is the discriminator, not this field:
graph_t::dispatch_edgetakes its remote leg on a non-empty link and reads this route without testing it. The two agree because the admitting door enforces it —graph_t::subscribe_wirerefuses an empty route asINVALID_PATH(#1055), and the:subscribers[]field-write arm, which binds no route, deliberately leaves link empty (see the note at that door: assigning a link there would manufacture exactly the routeless delivery this invariant excludes). So on a published edge the two are populated together or not at all, and testing either one answers “is this subscriber remote?”. Held as a view over a REFCOUNTED segment (ADR-0041 §2): copied once at subscribe, then every delivery snapshot is a refcount clone — O(1) copies over the subscription’s life, and an in-flight delivery keeps the route alive across a concurrent unsubscribe. An opaque view, so L4 never depends on tr::net.
-
view::view_t reverse_route = {}¶
The COMPLETED reverse-direction bound route (RFC-0024 §7.1 amendment 1) — a
PATH_REFTLV whose element 0 is THIS node’s own reference to the connection vertex the subscribe arrived on; empty ⇒ the subscription is canonical-only.Stored at admission by
graph_t::subscribe_wirewhen the mint-flagged subscribe carried a reverse list the responder could complete. On every delivery the producer consumes element 0 locally — validates it against its OWN vertex map (§6.2’s re-check) and egresses through the vertex it dereferences to — and puts elements1..on the wire as the delivery’s bounddst. A failed local validation (the link re-dialled; the generation moved) falls back to the canonical return_route, which is always stored alongside — the reverse binding is an optimisation plus a liveness check, never the only route. Same ownership shape as return_route — one refcounted copy at subscribe, refcount clones per delivery snapshot.
-
std::string caller¶
The caller context this edge was created under (#81, ADR-0026 fan-in gate).
The inbound link NAME for a remote subscribe, empty for a locally-wired edge. A fan-out re-dispatch into a LOCAL target vertex is gated by the TARGET’s
:aclWRITE right under this context — the subscription’s creator is the “writer” the target authorizes. A REMOTE subscriber’s fan-in gate runs on the peer instead (itsFWD{WRITE}terminus checks the same right).
-
bool delivery_compact = false¶
Route-handle opt-in (
SUBSCRIBER.qos_settings.delivery_compact, RFC-0004 §E.1 / ADR-0035 slice 4).When true the consumer requests label-compacted deliveries: the producer MAY advertise a per-link label aliasing this subscriber’s return route and thereafter stream lean COMPACT frames instead of full-route
FWD{WRITE}deliveries. Default false ⇒ stateless full-route delivery, so a cold/one-shot flow allocates no label state.
-
view::detail::ref_count_t refs = {1}¶
Intrusive refcount (#1442): how many holders name this record — the slot, plus one per PUBLISHED edge array whose entry points at it.
Rides the record’s existing TAIL PADDING and therefore costs zero bytes. delivery_compact ends at offset 113 and the record is 8-aligned, so a 4-byte counter lands at 116 and
sizeofstays the pinned 120 B. That is why the shape is an intrusive count and not astd::shared_ptr: a 16-byte handle would have widened subscriber_t (pinned at 80 B) AND pub_edge_t, whose width was measured at +23 % on the fan-out-1024 publish the last time it grew — the fix would have been paid for out of the delivery path.Not a synchronization primitive for the PAYLOAD. The payload is written once, before the record is ever named by a published array, and the seq_cst exchange that publishes that array is what makes those bytes visible to a pinned reader — exactly the ordering the deep copy relied on. The COUNT is atomic because it is genuinely contended: two mutators can be inside
scan_retired_edgesat the same time (each pops a disjoint retire list after releasing the stripe lock) and both may drop the last reference to the same record — and since #1448 the pinned reader touches it too: the dispatch snapshot (vertex_t::copy_published) CLONES the handle, a relaxed increment taken while the pin guarantees the entry’s own reference still holds the count above zero.tr::view::detail::ref_count_tis the in-tree primitive tr::view::segment_ptr_t already uses, guarded binding included (a core with no atomic RMW, #1722).
-
std::string link¶
-
struct pub_edge_t¶
One entry of a PUBLISHED edge array: the hot dispatch fields plus a liveness bit.
Written once, before the array is published, and never touched again — that is what lets a reader copy it out with no lock. The
activebit is the ONE mutable word, and it is MONOTONE: it starts true and an unsubscribe (vertex_t::clear_edge, vertex_t::evict_link_edges, retirement) flips it to false under the stripe lock. A reader loads it and skips the entry.That single mutable bit is not a hedge on immutability, it removes a failure mode. Without it every unsubscribe would have to BUILD a smaller array, and an unsubscribe that cannot allocate would be left publishing an edge the caller has already torn its
callback_ctxdown behind. With it, dropping an edge is allocation-free and therefore infallible; the compaction that actually reclaims the dropped entry’s refcount clones rides the next successful publish, where a failure costs nothing but a delayed release.Public Members
-
subscriber_fn_t callback = nullptr¶
In-process sink fn (null ⇒ target-only).
-
void *callback_ctx = nullptr¶
The sink’s caller-owned context.
-
target_key_t target_key¶
Local re-dispatch target (refcount share).
-
target_binding_t binding = {}¶
The minted slot for that target (#830).
-
remote_ptr_t remote¶
The cold wire half (null for a local edge) — a refcount SHARE of the admitting slot’s subscriber_remote_t, never a copy of it (#1442).
The entry’s WIDTH is the fan-out copy loop’s bandwidth, which is why this half is out of line at all: inlining its members made an entry 136 B against
subscriber_t’s 72 and cost +23 % on the fan-out-1024 publish — measured, not predicted. Sharing keeps that width exactly where it was (one pointer, as thestd::unique_ptrhere was) while making a republish’s per-entry cost a pointer copy and an increment instead of a heap allocation and twostd::stringcopies.
-
std::atomic<bool> active = {true}¶
Monotone true -> false liveness bit.
-
subscriber_fn_t callback = nullptr¶
-
using tr::graph::subscriber_fn_t = void (*)(void *ctx, const value_t &value)¶
The in-process per-edge delivery sink: a plain
{fn, ctx}pair (the ADR-0047 hot-path shape, same doctrine astr::net::receiver_slot_t).Snapshotting one under the fan-out lock is a trivial copy — no per-publish
std::functioncopy (which heap-allocates once captures exceed the SBO). The value crosses as the published block it is (value_t, RFC-0028 §5.1) — no wrapper, no copy; the sink reads its links in place and may clone them (value_t::rope, refcount bumps) if it keeps them past the call.
-
using tr::graph::subscriber_release_fn_t = void (*)(void *ctx)¶
The hook a caller hands graph_t::unsubscribe so the LIBRARY can signal it that the retired subscription’s context is dead.
The direction is the whole point of ADR-0080 §Decision 4: the embedder never polls in-flight state and never waits. It registers this, and libtracer calls it exactly once, on the caller’s own thread, outside every graph lock, at the policy’s grace point. A contract of the form “the callback may still be invoked until you call X” is what that decision rejects.
- Param ctx:
The
callback_ctxthe subscription was admitted with, handed straight back.
-
struct remote_delivery_t¶
What the producer fan-out hands a remote subscriber’s delivery sink (#136).
A pure description of one remote subscription edge: the consumer’s accumulated return route and this node’s NAME for the link it arrived on, both opaque to L4, plus the
vertex_t::subscriber_tdelivery_compact opt-in. The injected sink (atr::netconcern — graph_hooks_t::remote_delivery) interprets these: it maps link to a transport child and emits a full-routeFWD{WRITE}or, when delivery_compact, an auto-promoted labelCOMPACT(RFC-0004 §D/§E.1). link is borrowed for the sink call only; return_route is a refcount clone of the stored route segment (ADR-0041 §2) — the sink may rope it into an egress frame, and it stays alive across a concurrent unsubscribe.Public Members
-
std::string_view link¶
This node’s NAME for the consumer link.
-
view::view_t reverse_route¶
Completed reverse bound route (
PATH_REFview, refcount clone; empty ⇒ canonical-only). Element 0 is this node’s own reference, consumed locally by the sink per delivery — RFC-0024 §7.1 amendment 1.
-
std::string_view caller¶
The edge’s stored ACL fan-in context (#81) — the subject the sink’s local element-0 consumption re-checks §6.2 under.
-
bool delivery_compact = false¶
Opt-in to label-compacted delivery.
-
std::string_view link¶
-
struct sub_event_t¶
One EXTERNAL mutation of a producer’s
:subscribers[]— what graph_hooks_t::subscription_observer reports.“External” is exactly the ADR-0018 caller context being NON-EMPTY: the op arrived through
op_resolver_tcarrying an inbound link NAME. It is the same discriminator the SUBSCRIBE gate already runs under, so an observer sees precisely the set of edges a remote peer caused and never the ones the owner’s own wiring code made. The local doors — bothsubscribe()sugars,unsubscribe(), and a:subscribers[]field-write under the empty context — are deliberately silent: the host that called them already knows.Both path fields are CANONICAL KEYS (concatenated NAME records — the
PATHpayload, docs/reference/03), never a slash-spelled string: that is the form the graph addresses by, and rendering one is the consumer’s choice, not a cost the event imposes. Both are BORROWED for the duration of the callback only — copy what outlives it.Public Types
Public Members
-
wire::key_view_t producer¶
Canonical key of the PRODUCER — the vertex whose
:subscribers[]changed.
-
wire::key_view_t target¶
Canonical key decoded from the
SUBSCRIBER’sPATHchild — WHAT the record says, verbatim.EMPTY when the record carries no well-formed
PATHat all (a bare remote subscriber, whose consumer is named only by its return route over link).Warning
Read it as the SPELLING the record carried, not as a local vertex. On a wire subscribe it is one of two things and the event cannot tell them apart: a path through one of THIS node’s mounts, which
subscribe_wireresolves and binds the edge to (RFC-0021 §4.B.1), or the consumer’s address at ITS OWN root, which resolves to nothing here and is dropped as a re-dispatch target — delivery then rides the return route (RFC-0004 §D). On a local-target append it IS a key in this graph. The three are not distinguishable from the event alone; link tells the observer which transport the op came from, and the app’s own wiring says the rest.
-
std::string_view link¶
This node’s NAME for the transport link the op arrived on. Never empty.
-
std::size_t slot = 0¶
The
:subscribers[]slot index the event concerns (RFC-0009 §D.2 stable).
-
wire::key_view_t producer¶
-
using tr::graph::sub_observer_fn_t = void (*)(void *ctx, const sub_event_t &event)¶
The app-installable external-subscription observer.
Note
The ADR-0047
{fn, ctx}shape, NOT astd::function(#1049) — seesubject_resolver_fn_tfor why.ctxis caller-owned and must outlive every subscription mutation the graph can still report.Warning
Runs SYNCHRONOUSLY on the resolver’s thread, inside the operation it reports, and the reply is not assembled until it returns — so it must be cheap and non-blocking, and it MUST NOT re-enter
graph_t. It is called outside every graph lock (the admission door has already released the vertex stripe lock and the map lock), so a re-entrant call does not self-deadlock; it is refused on the simpler ground that an observer which mutates the graph while a:subscribers[]write is mid-flight makes the event stream depend on its own side effects. Deferral — queueing the event and acting on it from the app’s own task — is the APP’s job, exactly as it is for graph_hooks_t::remote_delivery.
The :stats census seam (RFC-0010 Amendments 1–2)¶
-
struct stats_counter_t¶
One member of a
:statscensus block — a noun and its value (RFC-0010 Am. 2).The noun is BORROWED and must be a literal (or otherwise outlive the sampling call): the block is encoded before the sampler’s frame is left, and nothing copies the string.
-
struct stats_block_t¶
One sampled seam block, filled by a
stats_sampler_fn_t(RFC-0010 Am. 2).A fixed-capacity, allocation-free carrier: the net plane samples INTO it and L4 encodes it, so the whole census keeps ONE encoder and one wire shape (RFC-0010 Am. 1 §D.3), and the sampling half never touches an allocator on a path a peer can drive.
kMaxMembers is a compile-time ceiling on how many nouns one seam may publish, not a protocol limit: it is sized to the widest block the reference net plane serves (
router_stats_t’s seven) with headroom, and a seam that outgrew it would be split rather than truncated — add refuses silently past the ceiling exactly so a caller cannot emit a half-member.Public Functions
-
inline void add(std::string_view noun, std::uint64_t value) noexcept¶
Append one member; a no-op once kMaxMembers is reached.
- Parameters:
noun – Borrowed, and must outlive the sampling call.
value – The counter’s value.
Public Members
-
std::array<stats_counter_t, kMaxMembers> members = {}¶
The filled prefix.
-
std::size_t count = 0¶
How much of it is filled.
Public Static Attributes
-
static constexpr std::size_t kMaxMembers = 12¶
The per-seam member ceiling — see the class brief.
-
inline void add(std::string_view noun, std::uint64_t value) noexcept¶
-
using tr::graph::stats_sampler_fn_t = bool (*)(void *ctx, std::string_view seam_class, std::string_view seam_name, stats_block_t *out)¶
Sample one NET-PLANE
:statsseam — the sixth{fn, ctx}seam the router installs UP into the graph (RFC-0010 Amendment 2, #1503 residual).Amendment 1 §D.4 drew the census boundary at the graph, because L4 cannot reach DOWN into the net plane to sample a router or a link. This seam inverts the direction instead of the dependency: the router, which already knows the graph, registers a sampler UP — the same shape as
graph_hooks_t::remote_deliveryand the four resolver seams its constructor installs — so L4 still names nothing below it.Note
Called ONLY from the cold
:statsread path, never on a hot path. It MUST NOT re-entergraph_t, andctxmust outlive every read the graph can still serve — the lifetime the router’s other five seams already require.- Param ctx:
The caller-owned context installed beside the function.
- Param seam_class:
The seam CLASS —
router,labelsorlink(Amendment 2 §D.4).- Param seam_name:
The seam NAME within that class; for
linkit is the router’schild_registry_tname.- Param out:
Where to write the block, or
nullptrfor a RECOGNITION PROBE: answer whether the spelling names a seam and sample nothing.- Return:
truewhen the spelling names a seam this sampler serves.falseis the node’s “this seam is not published here” —SCHEMA_NOT_FOUND, and per Amendment 1 §Compatibility a monitor MUST read it that way, never as an error.
Handlers and delivery policy¶
-
struct write_ctx_t¶
The per-call context a write carries into a HANDLER’s
on_write(#375).A HANDLER is the one seam where application code REACTS to a write, so it is the one seam that needs to know WHO wrote. The graph already resolved that identity one stack frame earlier — the ACL gate (
graph_t::acl_allows) runs immediately before the handler, on the same value — so this type hands the handler the datum the gate just used rather than making it re-derive one. It is the ACL subject-table integration point (ADR-0018 — authorization over a pluggable subject token; ADR-0082 for why the subject is a claim of its own and not a spelling ofpeer_named): a handler that keys its own policy off subject keys it off exactly what the vertex’s:aclwas evaluated against.Note
This sentence used to cite RFC-0010, which is the wrong document — RFC-0010 is owner-writable application property fields (the field descriptor table, the reserved
settings.appnamespace and owner-defined:schema) and says nothing about subjects or access control. The subject-token model is ADR-0018’s; the decoupling of that token from peer addressing is ADR-0082’s, which itself points back at this comment as the integration point it feeds. Corrected with #375 Part 2.Warning
LIFETIME — subject is BORROWED for the duration of the call, the SAME contract the
rope_t&alongside it carries: COPY IF RETAINED. It views bytes owned by the router’s inbound frame or by the caller’s own storage, and both are gone the momenton_writereturns. Stashing thestring_viewin a member, a map key, or a queued work item is a DANGLING read, not merely a stale one. Take astd::string(or the token’s bytes) if the identity must outlive the call.Public Functions
Public Members
-
std::string_view subject¶
The resolved SUBJECT token of the writer — the ACL model’s
subject → rightsprincipal (CONTEXT.md §Access control, ADR-0018).EMPTY means the LOCAL HOST: the owner’s own in-process write through the graph API. That is not a magic string but the very discriminator the ACL gate runs on — the empty caller context is the trusted-by-convention channel
graph_t::acl_allowsshort-circuits BEFORE any resolver runs (#905), and a remote writer, which always carries a non-empty context, cannot spell it. Prefer is_local_owner to comparing against"".Note
There is no
OWNER@sentinel and there must not be one: ADR-0020’s erratum (#1033) withdrew that name because no evaluator ever special-cased it, so anOWNER@ACE matched nobody and LOCKED the vertex it was written to delegate. The owner sentinel here is the EMPTY token, which no ACE can spell.Note
Non-empty, this is the operation’s caller context exactly as the gate saw it. The token is PLUGGABLE (ADR-0018, ADR-0045 raw-key ed25519 TOFU) — a stronger credential slots in without changing this seam or the ACL model.
-
const net::link_kind_t *link = nullptr¶
The transport-catalog
(kind, role)of the LINK this write arrived on (#1650); null when it arrived over none.subject says WHO wrote; this says over WHAT. A policy that must treat a session a
wslistener accepted differently from a peer link this node dialled — the same write, the same vertex — testslink->is("ws", net::conn_role_t::LISTEN)rather than inferring the kind from a link name. It is fixed once per link at registration and costs a frame one pointer, read from the router’s per-link context the subject is derived from.NULL means no catalogued link carried THIS write, which is three cases a filter tells apart through the subject (subject):
the owner’s own API write (is_local_owner);
a delivery landing here from a SUBSCRIPTION EDGE (a fan-in write). Such a write runs under the edge’s stored subject, which was gated when the edge was admitted; the edge does not carry its creator’s link kind (the edge record does not grow for it);
a write over a link registered without a catalog identity (a link added to the router directly rather than through
transport_vertex_t).
A filter whose policy depends on the link kind therefore decides the null case explicitly for a non-owner subject, rather than reading null as either kind.
Warning
BORROWED for the call, like the subject — copy the pair out if the decision must be remembered past the return.
-
std::string_view subject¶
-
template<class Sig>
struct hook_t¶ THE graph callback idiom (RFC-0028 D10): a
{fn, ctx}pair over the call signatureSig. Declared only; theR(A...)specialisation below is the definition.
-
template<class R, class ...A>
struct hook_t<R(A...)>¶ A
{fn, ctx}callback slot for the signatureR(A...).An aggregate:
{fn, ctx}builds one, and a captureless lambda whose first parameter isvoid*converts to fn_t implicitly. Default-constructed it is EMPTY (fn == nullptr), which every seam treats as “not installed”. Trivially copyable — copying a hook copies two words and never allocates.Public Types
Public Functions
-
inline explicit operator bool() const noexcept¶
True iff a callback is installed.
-
inline explicit operator bool() const noexcept¶
-
template<class F>
thunk_t<F> tr::graph::thunk(F &f) noexcept¶ Point a hook_t at a callable (RFC-0028 D10’s
tr::graph::thunk<F>).The hook stores
&f, sofmust outlive every call made through it. For a seam that only needs an object you already own (this), prefer the two-word aggregate{captureless_lambda, this}: nothing extra to keep alive.
A hook is a STORED callback whose context the caller keeps alive. A callback that is only
called before the function taking it returns takes the non-owning function_ref_t instead
(ADR-0083 Decision 9,
#1776): two words, no allocation, and
it binds to a temporary lambda.
-
template<class Sig>
class function_ref_t¶ Primary template; only the function-type specialization is defined.
-
template<class R, class ...Args>
class function_ref_t<R(Args...)>¶ A non-owning, trivially copyable reference to a callable of signature
R(Args...).Holds the callable’s address and a trampoline; a function pointer is held by value. It owns nothing, so the referenced callable must outlive every call made through it. That is always true for a parameter (a temporary lives until the end of the full expression that holds the call) and is the caller’s burden anywhere else, so do not store one; store a callable with inline storage, or a hook, instead.
Never empty: there is no default constructor and no null state, so a call needs no check.
- Template Parameters:
R – The return type.
Args – The parameter types.
Public Functions
-
template<class F>
inline constexpr function_ref_t(F &&f) noexcept¶ Refer to
f, which is called as a non-const lvalue (or a const one, whenfis const).
-
template<class Fn>
inline function_ref_t(Fn *f) noexcept¶ Refer to the function
f, held by value. Precondition:fis not null.
-
template<class F>
function_ref_t &operator=(F&&) = delete¶ Deleted: assigning a callable would leave this reference pointing at an object that dies at the end of the statement. Assign another
function_ref_t.
-
struct handlers_t¶
User behavior for a Handler-role vertex — six hook_t seams, 96 B on the host (RFC-0028 D10: one callback idiom).
on_childrenadditionally applies to ANY role: when set, a read of the vertex’s:children[]field serves this synthesized member listing (a complete POINT TLV view) INSTEAD of enumerating registered child vertices — the ADR-0044 seam by which a transport/connection vertex lists its live bus peers without ever creating a vertex for them.on_readsupplies the vertex value as a value_ref_t (RFC-0028 D11, one read type): a handler that holds a value already (a cached reading, a value it kept withvalue_ref_t::keep) answers a reference to it at no allocation, and one that computes a scalar mints it withvalue_ref_t::copy— one block, the bytes inline. The graph hands the reference back fromgraph_t::read/graph_t::awaitunchanged;on_writeandon_admitreceive the written value as the value_t the write path already holds — by reference, with no clone of its links.Every seam is a
{fn, ctx}pair whosectxthe CALLER keeps alive for as long as the vertex is registered (seefor the two idiomatic spellings and tr::graph::thunk). An empty hook is an uninstalled seam.libtracer/hook.hppThe RFC-0014 Amendment 2 payload-right rows are not a seam and are not carried here: they are the trailing
rightsargument ofgraph_t::register_vertexand its siblings.Public Members
-
hook_t<result_t<value_ref_t>()> on_read¶
Supplies the vertex value on read, as an owning reference (RFC-0028 D11).
An empty reference on success is read as a refused allocation and answers
BACKPRESSURE, the same answer avalue_t::make*that returnednullptrdeserves.
-
hook_t<result_t<void>(const value_t &value, const write_ctx_t &ctx)> on_write¶
Receives the written value and the writer’s write_ctx_t (#375).
Warning
Both arguments are BORROWED for the call. The value is the one the write path holds — for a delivery from a subscription edge, the very block the source published (RFC-0028 D2: a HANDLER target adopts like a stored target, no per-handler copy); for a relay or a local write it may be storage on the writer’s stack. A handler that keeps the value past its return takes
value_ref_t::keep(value)— a refcount share of a published block, a copy of the links out of stack storage — and NEVER keeps the reference or its address.
-
admit_hook_t on_admit¶
The ADMISSION seam of a RETAINING vertex: runs BEFORE the write becomes state, and decides whether — and in what form — it does (
admission_t).The gap it closes.
on_writeis the HANDLER role’s seam, and a HANDLER retains nothing: choosing it to validate a write meant giving up the last-known-value, theawaitwake and the whole composed-read surface that make aSTORED_VALUEthe graph-authoritative form of a datum. So a consumer had to pick RETENTION or VALIDATION and could not have both. This seam is the same refusal power on the storing roles, taken at the one place every store goes through (graph_t::store_value) rather than bolted onto one door.WHERE it runs, exactly: after the write gate’s ACL decision, before
vertex_t::store, therefore before the sequence bump, before anyawaitwake, before a STREAM ring admission and before ANY subscriber delivery — local, bubbled or remote. A refused write is unobservable except as the writer’s error; a normalised one is observable ONLY in its normalised form.WHICH writes it sees: every write that would STORE at this vertex, whatever the door.
writeandassignalike, aFWD{WRITE}terminus, a delivery landing here from an inbound edge (the fan-in write), and this vertex’s own slice of a branch-POINT decomposition. Admission is a property OF THE VERTEX, not of a channel — a vertex whose invariant only held against remote writers would not hold. The owner’s own writes are included and are told apart bywrite_ctx_t::is_local_owner, which is what a filter that wants to admit the owner unconditionally tests.WHICH it does NOT see: a HANDLER-role vertex (
on_writeis its seam and already has this power — installing both, the role’son_writeruns and this does not), and the app-field plane, which is a different plane with its own seam (on_app_field_admit).COST. Unset ⇒ nothing: the store path tests one bit of a flags word the write path already holds and never loads the seam block. That bit is the whole per-vertex cost.
Warning
Both arguments are BORROWED for the call — the same contract
on_writecarries, includingvalue_ref_t::keepfor a filter that retains the value. The seam runs on the WRITER’s thread with no vertex lock held, so it may re-enter the graph, and it is on the hot write path: a filter that blocks blocks the writer.
-
app_field_admit_hook_t on_app_field_admit¶
The app-field plane’s admission seam (RFC-0010 §A.3), the field-shaped twin of on_admit — runs BEFORE a declared
:settings.app.<name>write stores its bytes, and may refuse it or normalise it.Called with the field’s key (below
settings.app.), the written TLV and the writer’s write_ctx_t, after the ACL gate and after the RFC-0010 §A.3 writability check, beforeapp_field_store. The context is the one on_admit receives (#1832): the subject the ACL gate ran on, and the arrival link’s(kind, role)— null for the owner’s own write and for a field write no catalogued link carried. Return the view handed in to store it verbatim (the pre-existing behaviour), a DIFFERENT view to store those bytes instead, orstd::unexpected(status)to refuse — in which case nothing is stored, the field keeps its prior bytes, on_app_field_write does NOT fire, and the status is the writer’s answer.Warning
A returned view is READ during the call that returned it — the store copies the bytes out before returning — so it may point at storage the filter owns, but that storage must outlive the return. The context is BORROWED for the call, as for on_admit. Unset ⇒ bytes store verbatim, as before.
-
app_field_write_hook_t on_app_field_write¶
The owner apply seam (RFC-0010 §A.3): fires after a declared
:settings.app.<name>field write stored its bytes, with the field’s key (belowsettings.app.) and the written TLV — OUTSIDE the vertex lock, so it may re-enter the graph (apply the config, restructure children, then ANNOUNCE the change with an ordinary data write per §C — the field write itself never wakesawaitand never propagates). Unset ⇒ the bytes just store (a passive metadata field).
-
hook_t<result_t<value_ref_t>()> on_read¶
-
struct value_handlers_t¶
The internal, lazily-allocated STORAGE of a vertex’s VALUE seam (ADR-0058 Step 2) — the seams
handlers_tcarries minus the app-field ones and the admission filter: three hook_t pairs, 48 B on the host.Split off from the public handlers_t input so a vertex that installs none of the three never allocates this block: it lives behind a lazily published pointer in the extension block, null unless at least one of
on_read,on_write,on_childrenwas given. Allocation is keyed on that PRESENCE, not onrole_t—adopt_identitynever consults the role — so aSTORED_VALUEvertex given anon_children(the/net/<module>/<name>identity vertex of a bus link) does carry one, and aHANDLERvertex registered with an empty handlers_t does not. Which of the three is ever CONSULTED is a separate, per-seam question:on_read/on_writerun only on a HANDLER-role target, whileon_childrenserves the synthesized listing whatever the role.on_app_field_writeco-occurs with app fields, not the value seam, so it moved to app_field_group_t. The two ADMISSION filters live on the GRAPH, not here, for the reasongraph_t::admissions_states — the same reason the payload-right rows do. Set once at registration (vertex_t::adopt_identity), read lock-free thereafter.Public Members
-
hook_t<result_t<value_ref_t>()> on_read¶
Supplies the vertex value on read — the handlers_t::on_read contract.
-
hook_t<result_t<void>(const value_t &value, const write_ctx_t &ctx)> on_write¶
Receives the written value and the writer’s write_ctx_t (#375) — the handlers_t::on_write contract, verbatim.
-
hook_t<result_t<value_ref_t>()> on_read¶
-
struct delivery_policy_t¶
One subscription’s DELIVERY policy — a packed 16-bit field carried in the
SUBSCRIBERTLV’sSETTINGSchild (RFC-0022 §3.A).Delivery policy describes one producer→subscriber relationship, not the producer: a vertex that fans out to a CAN peer and a WebSocket peer at once has no single
reliabilityorpriorityto hold, which is why these lived on the vertex for a year without anything ever consuming them. DDS puts the same three on the reader/writer pair for the same reason.bits
field
values
0–1
reliability
0 = best-effort, 1 = reliable; 2–3 reserved
2–4
priority
0–7, 0 = default
5
durability_request
1 = deliver the latched last value on join
6–7
delivery_class
0 = conflate (default), 1 = immediate, 2 = batch, 3 = stream
8–15
reserved
MUST be written 0, MUST be ignored on read
Absent from the wire ⇒ all-zero ⇒ today’s default behaviour, byte-identically — and the class field costs no wire byte for the same reason:
0is conflate, which is what every pre-RFC-0025 subscriber wrote into those bits when they were reserved. Old subscribers are conflate-class BY CONSTRUCTION.Only durability_request is consumed today (the transient-local latch at
graph_t::admit_subscriber). priority and delivery_class are stored and read back, awaiting the work that honours them — the honest shape RFC-0022 §3.E chose over moving dead per-vertex fields. reliability is not awaiting anything: RFC-0025’s 2026-08-24 §4.4 selector erratum (#1204) rules the pressure arm to be the RECEIVING vertex’s own declaration (vertex_policy_t::ring_source), so these two bits are carried verbatim and read by nothing — decoded, stored, read back from:subscribers[]and re-emitted unchanged, with no behaviour behind them and none promised.Flags only, never a magnitude. A deadline or a queue bound added later is a magnitude and belongs in the subscription’s cold half as a full-width field, never in these bits.
Public Functions
-
inline constexpr std::uint8_t reliability() const noexcept¶
0 = best-effort, 1 = reliable (2–3 reserved; stored, never interpreted).
-
inline constexpr std::uint8_t priority() const noexcept¶
0–7, 0 = default.
-
inline constexpr bool durability_request() const noexcept¶
True iff THIS subscriber asked for the latched last value on join.
-
inline constexpr delivery_class_t delivery_class() const noexcept¶
Bits 6–7 — how the fan-out edge treats this subscriber’s deliveries (RFC-0025 §4.1).
Every two-bit pattern is an assigned class, so this accessor is total: there is no “unknown class” to reject, and a word from a future sender still decodes to one of the four. Reading the field is not honouring it — the classes beyond
CONFLATEland with the fan-out-edge mechanics and the receiving vertex’s ring.
-
bool operator==(const delivery_policy_t&) const = default¶
Memberwise equality on the raw bits (reserved bits included — they are carried verbatim, so two policies differing only there are not the same bytes).
Public Members
-
std::uint16_t bits = 0¶
The packed field, as it arrived off the wire.
Public Static Attributes
-
static constexpr std::uint16_t kReliabilityMask = 0x0003¶
Bits 0–1.
-
static constexpr std::uint16_t kPriorityMask = 0x001C¶
Bits 2–4.
-
static constexpr int kPriorityShift = 2¶
Bits 2–4 offset.
-
static constexpr std::uint16_t kDurabilityRequest = 0x0020¶
Bit 5.
-
static constexpr std::uint16_t kDeliveryClassMask = 0x00C0¶
Bits 6–7.
-
static constexpr int kDeliveryClassShift = 6¶
Bits 6–7 offset.
-
inline constexpr std::uint8_t reliability() const noexcept¶
-
enum class tr::graph::delivery_mode_t : std::uint8_t¶
Per-VERTEX propagation policy (value-agnostic; RFC-0008 §C).
Governs whether an ANCESTOR’s propagate sweep includes this vertex — NOT a per-subscriber value filter (there is no byte comparison; ADR-0053 §1, a vertex never parses its bytes).
assignand a DIRECT propagate on the vertex itself are never gated by it. Held as vertex state (default IF_NEWER); wire config via the vertex:settingsis deferred. Numeric filtering (deadband) remains an application filter vertex (ADR-0021 sibling), never a field here.Values:
-
enumerator IF_NEWER¶
Default: an ancestor sweep includes this vertex only if it was assigned since the last covering sweep — the structural coalescing flush (RFC-0008 §B).
-
enumerator UNCONDITIONAL¶
An ancestor sweep ALWAYS includes this vertex’s current value (a sweep-driven keepalive; the producer’s timer sets the rate).
-
enumerator EXPLICIT¶
An ancestor sweep NEVER includes it; deliverable only by a direct propagate on the vertex itself.
-
enumerator IF_NEWER¶
-
class value_t¶
One published value: an intrusive refcount, its source, and its link chain, in one block (RFC-0028 §5.1).
Never constructed directly — make draws the block from a
block_source_tand places the header and the links in it; the last release destroys the links and hands the block back to that source. The read surface is the read-only half ofrope_t’s, so a consumer that used to receiveconst rope_t&reads aconst value_t&the same way.Public Functions
-
inline bool is_loaned() const noexcept¶
Whether this value’s header lives in a loaned receive block (RFC-0028 §6.9).
-
inline bool is_inline() const noexcept¶
Whether this value’s bytes live in its own block (the copy arm).
-
inline std::span<std::byte> inline_bytes() noexcept¶
The inline bytes, WRITABLE — for the maker to fill between make_inline and publication. Precondition: is_inline.
-
inline void retain() const noexcept¶
Take one more reference. Relaxed: a holder retaining already owns one.
-
inline std::uint32_t use_count() const noexcept¶
The current reference count (a snapshot; for tests and accounting).
-
inline mem::block_source_t *source() const noexcept¶
The source the block is released to, or
nullptrfor storage the caller owns (value_storage_t).
-
inline std::size_t block_bytes() const noexcept¶
The block’s size in bytes: inline_bytes_for its length for an inline value, else bytes_for its link count.
-
inline std::size_t link_count() const noexcept¶
Number of links in the chain.
-
inline const view::view_t &only() const noexcept¶
The single link of a one-link value. Precondition:
link_count() == 1.
-
inline std::size_t total_length() const noexcept¶
Total payload bytes across the chain.
-
inline bool all_host() const noexcept¶
True iff every link is in HOST space (CPU-readable).
-
inline view::view_t materialize(mem::mem_backend_t &backend = mem::heap_backend()) const¶
One contiguous view of the value: the link itself when there is one, else a flattened copy through
backend.
-
inline std::expected<view::view_t, view::flatten_err_t> try_materialize(mem::mem_backend_t &backend = mem::heap_backend()) const¶
The nothrow twin of materialize — the flatten’s refusal comes back by value.
-
inline view::view_t flatten(mem::mem_backend_t &backend = mem::heap_backend()) const¶
One contiguous copy of the whole payload through
backend(rope_t::flatten).
-
inline std::expected<view::view_t, view::flatten_err_t> try_flatten(mem::mem_backend_t &backend = mem::heap_backend()) const¶
The nothrow twin of flatten — the refusal comes back by value.
-
inline std::vector<std::span<const std::byte>> to_iovec() const¶
The links as byte spans, in order — the scatter-gather shape a link’s
sendtakes. Allocates the vector; try_to_iovec is the nothrow spelling.
-
inline bool try_to_iovec(std::vector<std::span<const std::byte>> &out) const noexcept¶
Nothrow to_iovec into
out(cleared first).- Return values:
false –
outcould not be reserved; it is left empty.
-
inline view::rope_t rope() const¶
A
rope_tover this value’s links — one refcount clone per link.The bridge to every seam that speaks
rope_t(egress, decode, a sink that keeps the value past the call). A chain longer than the rope’s inline buffer allocates the rope’s spill; try_rope is the nothrow spelling for a hot leg.
-
inline bool try_rope(view::rope_t &out) const noexcept¶
Nothrow rope into
out:reserves the chain first, so the appends cannot reallocate.Out of line: until RFC-0028 slice 4 this was the delivery clone
dispatch_edge_targettook once per bound edge, and inlining the reserve-then-append loop there cost that pinned symbol ~100 B on the symbol ratchet. A target now ADOPTS the published block, and the adoptingstore_valuetakes this clone only where it cannot adopt (a HANDLER target, caller-owned storage) or to show an admission filter the links. One call keeps each of those bodies’ shape; the loop itself is the same either way.- Return values:
false – The chain could not be reserved —
outis left as it was.
Public Static Functions
-
static inline constexpr std::size_t bytes_for(std::size_t links) noexcept¶
The block size a value with
linkslinks occupies — header plus chain.This is what a publish costs in bytes and what release hands back to the source.
-
static inline value_t *make(view::rope_t &&links, mem::block_source_t &source) noexcept¶
Mint a value over
links, MOVING the chain out of the rope — no refcount traffic on the links.One
try_alloconsource, nothrow (#477): exhaustion isnullptrandlinksis left intact, so the caller still owns the chain it offered and can soft-fail.- Parameters:
links – The chain to publish. Emptied on success; untouched on
nullptr.source –
The block source the value’s block is drawn from and released to. Must outlive every reference to the value (the “handles do not outlive the
graph’s memory” contract the injection seam already imposes).
- Returns:
The value, holding ONE reference (the caller’s), or
nullptrwhensourcerefused the block.
-
static inline value_t *make(std::span<const view::view_t> links, mem::block_source_t &source) noexcept¶
Mint a value over a COPY of
links(one refcount clone per link).The spelling for a chain the caller does not own — a subview of an inbound frame, another value’s links. Same one-
try_alloc, nothrow contract as the rope form.
-
static inline constexpr std::size_t inline_bytes_for(std::size_t len) noexcept¶
The block size an INLINE value of
lenbytes occupies: header, its one link, the embedded segment, and the bytes — laid out by the placement module (tr::mem::inline_block_bytes).
-
static inline value_t *make_inline(std::size_t len, mem::block_source_t &source) noexcept¶
Mint an INLINE value of
lenbytes — the copy arm of the copy-or-share policy (RFC-0028 §5.1 / §5.3): ONEtry_allocofinline_bytes_for(len)onsource.The bytes are UNINITIALISED: the maker fills them through inline_bytes before it publishes the value or hands out a reference (the only window in which a value’s bytes may be written). Nothrow (#477): exhaustion is
nullptr.- Returns:
The value, holding ONE reference (the caller’s), or
nullptrwhensourcerefused the block orlendoes not fit the header’s count.
-
static inline value_t *make_copy(std::span<const std::byte> bytes, mem::block_source_t &source) noexcept¶
make_inline over a copy of
bytes— one block, onememcpy.
-
static inline value_t *inline_owner(const view::view_t &link) noexcept¶
The inline value whose embedded segment
linkis the WHOLE of, ornullptr.The backend identity says the segment lives inside a value block; the full-window test says the link is the value’s bytes and not a subview of them (a subview is a different value, and publishes as a link to this one).
-
static inline void release(const value_t *v) noexcept¶
Drop one reference; the last one destroys the links and returns the block to its source. Null-safe.
Acquire-release on the decrement, the canonical intrusive release: the thread that frees sees every write the other holders made before they released.
-
inline bool is_loaned() const noexcept¶
-
class value_ref_t¶
An owning reference to a vertex’s PUBLISHED value — what graph_t::read and graph_t::await hand back, and what every slot policy’s
load()returns.A read of a PUBLISHED value returns a reference to it; a read that COMPOSES a new value (graph_t::read_children_folded and its siblings, a field read) returns a reference to a fresh composed block (tr::graph::value_ref_t::composed) — every value read answers this one type (RFC-0028 D11), a HANDLER’s
on_readincluded. Measured when the rule was drawn, both arms alternating inside one binary on a 24-thread host: median 1.37x aggregate for the reference over a rope copy, and p50 improving most where it hurts most — 2,104 ns to 1,193 ns at sixteen readers on one shared vertex.Holding one keeps the value’s block alive — and under an injected
block_source_tthat is a real obligation: the block was drawn from the graph’s source, so an outstanding reference pins it (ADR-0069, deferred reclamation). A must not outlive the graph it was read from.Public Functions
-
inline value_ref_t(const value_ref_t &other) noexcept¶
A copy is one more reference to the same value.
-
inline value_ref_t(value_ref_t &&other) noexcept¶
A move takes
other'sreference and leaves it empty.
-
inline value_ref_t &operator=(value_ref_t other) noexcept¶
Copy-and-swap: the old reference is dropped as
otherdies.
-
inline void reset() noexcept¶
Drop the reference, leaving this empty.
-
inline const value_t &operator*() const noexcept¶
The referenced value. Undefined if this reference is empty.
-
inline explicit operator bool() const noexcept¶
Whether this reference names a value.
Public Static Functions
-
static inline value_ref_t adopt(value_t *v) noexcept¶
Adopt a reference the caller already holds (a fresh value_t::make, a slot’s retained load).
Take a NEW reference on
v(oneretain).
-
static inline value_ref_t composed(view::rope_t &&r) noexcept¶
Take ownership of a freshly COMPOSED value, giving it a published value’s shape.
The composed branch read builds a rope no vertex published; this is what lets it answer the same signature. It draws one block from the host value sub-pool (
mem::value_source(), #1777) — the composed value has no vertex, so no injected source — which the published path does not pay; measured neutral (1.00x over 30 paired samples), because a subtree walk dominates it.- Returns:
The reference, or an EMPTY one when the heap refused the block (#477).
-
static inline value_ref_t copy(std::span<const std::byte> bytes, mem::block_source_t &source = mem::value_source()) noexcept¶
Mint a value over a COPY of
bytes— the INLINE arm (value_t::make_copy): one block fromsource, the bytes in it, no segment of their own.The spelling a HANDLER’s
on_readuses for a value it computes (RFC-0028 D11): a scalar reading costs exactly this one block, where the rope it used to answer cost a segment for the bytes and a block to wrap it.- Returns:
The reference, or an EMPTY one when
sourcerefused the block (#477).
-
static inline value_ref_t keep(const value_t &v, mem::block_source_t &source = mem::value_source()) noexcept¶
Keep a BORROWED value past the call that lent it — the one way a seam that was handed
const value_t&(a subscriber callback,handlers_t::on_write,handlers_t::on_admit) retains it (RFC-0028 D10).A value drawn from a source (a published block — what a subscription edge delivers) is SHARED: one refcount bump, no allocation. A value with no source is caller-owned storage on the writer’s stack (
value_storage_t, the relay and local-handler shape), which dies when the call returns, so its links are cloned into ONE fresh block fromsource— the payload bytes are never copied, only the links’ references.Never keep the reference itself, or its address: a stack value’s storage asserts, on the way out, that nothing did.
- Returns:
The reference, or an EMPTY one when
sourcerefused the block (#477).
Friends
-
inline friend bool operator==(const value_ref_t &a, const value_ref_t &b) noexcept¶
Identity: two references to the same block.
-
inline friend bool operator==(const value_ref_t &a, const value_t *b) noexcept¶
Identity against a raw value pointer (
nullptrtests emptiness).
-
inline value_ref_t(const value_ref_t &other) noexcept¶
-
template<std::size_t N>
class value_storage_t¶ Caller-owned storage for a value of up to
Nlinks that is never published — the shape a branch write delivers a slice it did not store in (RFC-0005).The value lives in this object, so no source is drawn from and nothing is released: the header’s
sourceis null. It holds the ONE reference the storage owns; a consumer must not keep a reference past the storage’s lifetime (asserted in debug builds).Public Functions
-
inline explicit value_storage_t(std::span<const view::view_t> links) noexcept¶
Build over a copy of
links(one refcount clone per link). Precondition:links.size() <= N.
-
inline explicit value_storage_t(const view::rope_t &r) noexcept¶
Build over a rope’s chain. Precondition:
r.link_count() <= N.
-
inline explicit value_storage_t(std::span<const view::view_t> links) noexcept¶
-
class rx_loan_source_t : public tr::mem::block_source_t¶
The “source” of a value whose header lives in a loaned receive block (RFC-0028 §6.9, #1626) — it serves nothing and reclaims nothing.
A loaned value’s header sits in the receive block’s reserve (
mem::kRxLoanBytes), and the block goes back to ITS backend when the last segment reference drops, exactly as it did before the value existed. So there is nothing for a value source to hand out or take back:try_allocrefuses andreleaseis never asked. It exists as an IDENTITY — non-null, so a loaned value is a published block to every seam that tells a stack value from a shared one (value_ref_t::keep, the target adopt), and unique, so the last release knows which arm it is on.Public Functions
-
inline rx_loan_source_t() noexcept¶
The singleton’s name, as census and diagnostics print it.
-
inline virtual void *try_alloc(std::size_t, std::size_t) noexcept override¶
Refuses: a loaned header is never drawn, it is placed.
-
inline virtual void release(void*, std::size_t, std::size_t) noexcept override¶
Never reached: the loaned arm of the last release returns before it.
-
inline rx_loan_source_t() noexcept¶
-
class inline_value_backend_t : public tr::mem::mem_backend_t¶
The reclaimer of an inline value’s embedded segment (RFC-0028 §5.1, slice 5).
One process-wide instance, inline_value_backend. It allocates nothing: its only job is the last segment reference’s
destroy, which hands the WHOLE block — header, link, segment and bytes — back to theblock_source_tthe value was drawn from. It is also the identity that marks a segment as embedded: a segment whose backend is this one lives inside avalue_t’s block (value_t::inline_owner).Public Functions
-
inline inline_value_backend_t() noexcept¶
The singleton’s name, as census and diagnostics print it.
-
inline virtual void destroy(view::segment_t *seg) noexcept override¶
Return the enclosing value block to its source. Defined after
value_t.The last segment reference of an inline value: hand the whole block back.
The header outlives the value on purpose —
refs, the count andsourceare trivially destructible and nothing tore them down — so the reclaimer readssourcehere.
-
inline virtual std::size_t alignment() const noexcept override¶
The inline bytes follow the segment header, so they are aligned to it.
-
inline inline_value_backend_t() noexcept¶
-
inline inline_value_backend_t &tr::graph::inline_value_backend() noexcept¶
The one inline_value_backend_t.
Constructed on first use into static storage and NEVER destroyed: a value released during static destruction (a graph with static storage duration) still reaches a live reclaimer.
-
constexpr std::uint32_t tr::graph::saturate_threshold(std::size_t bytes) noexcept¶
A share threshold (RFC-0028 §5.3) as the 32-bit word
vertex_ext_tstores: anything fromUINT32_MAXup saturates toUINT32_MAX, which reads back asSIZE_MAX— copy always.
Edges¶
-
struct edge_view_t¶
The edge record’s WIDTH, pinned (#1266).
Both halves are per-edge storage on a node whose subscriber arena is measured in kilobytes, and the hot half is what the fan-out loop streams — #380 §3 split the cold members out for exactly that reason, and the split is only worth anything while the hot record stays narrow. These numbers moved silently more than once (a member added to the wrong half costs nothing a test can see until the RAM census runs), so they are stated where a change to either struct has to walk past them.
64-bit hosts only: the widths are pointer-sized-member sums, so an MCU build legitimately differs and a
sizeofpin there would be a false alarm rather than a guard.#1442 moved the cold half from owned-per-holder to refcount-shared and both numbers are unchanged: the handle is one pointer, as the
std::unique_ptrwas, and the intrusive count fits the cold record’s pre-existing tail padding. A future member that pushes subscriber_remote_t past 120 B evicts the counter into a word of its own and costs 8, not 4 — that is the growth this pin is here to price.The dispatch-relevant snapshot of one ACTIVE subscription edge — four words and two refcounts, no byte copy of anything (#1448).
What vertex_t::snapshot_edges copies out under an edge pin so the graph can dispatch with the pin released (callbacks / re-dispatch re-enter the graph): the
{fn, ctx}callback pair, the minted binding, and refcount SHARES of the two owned records — the target key and the whole cold half.Why the cold half is shared here and not copied (#1448). #1442 made subscriber_remote_t refcount-shared and immutable after admission, and #1447 spent that on the SUBSCRIBE path (
vertex_t::try_publish_edges). This snapshot is the same copy on the DELIVERY path, where it is paid once per edge per write rather than once per admission: it used to ownstd::stringcopies of the link and the caller plus refcount clones of the two routes, i.e. two probe-guarded assignments and two atomics for every remote edge of every fan-out. It is now ONE relaxed increment, and the record it names is exactly the bytes the copy used to reproduce. The lifetime guarantee the copies bought — the slot may be cleared while dispatch runs outside the pin — is bought instead by the share itself: this handle is a holder, so the record outlives the unsubscribe that drops the slot’s.That also makes the whole snapshot INFALLIBLE. Nothing in it can allocate (a
shared_ptrclone and an intrusive increment do not), sovertex_t::copy_publishedno longer has a per-edge OOM leg at all and vertex_t::snapshot_drops_t no longer carries anout_of_memorycount — the shed it used to describe cannot happen.ADR-0041 §2 is satisfied more strongly than before, not stretched: its remote-subscriber row asks for one copy at subscribe into a refcounted segment with every later delivery roping the stored route rather than copying it. The routes already complied as
view_tclones; now the delivery does not even clone them, and nothing here is a borrowed span — the handle owns.The width matters on its own: 160 B → 48 B. edge_snapshot_t is
kCapacityof these on the publishing thread’s stack, and a wide fan-out streamsFof them through the overflow vector, which is theF * sizeof(edge_view_t)termbench/bench_common.hppnames as the reason the mid fan-out ladder exists.Public Functions
-
inline std::string_view link() const noexcept¶
This edge’s remote-delivery link NAME; empty ⇒ no remote leg.
Borrowed from the record this view holds, so it is valid for as long as the view is — which is exactly as long as the
std::stringmember it replaces was. Empty for an in-process edge AND for the:subscribers[]field-write arm, which binds a caller context but deliberately no link (there is no return route to deliver over).
-
inline std::string_view caller() const noexcept¶
The edge’s stored ACL fan-in context (#81), borrowed from the held record; empty for a locally-wired edge.
-
inline bool has_remote_leg() const noexcept¶
Does this edge have a REMOTE-delivery leg — i.e. a non-empty link?
The gate
graph_t::dispatch_edgetakes per edge, kept as one named test because it is on the always-inlined per-edge body of the wide fan-out loop. The null check short-circuits, so the in-process edge — the bulk of any fan-out — pays one load and one branch, exactly whatlink.empty()cost when the string was inline.
Public Members
-
subscriber_fn_t callback = nullptr¶
The in-process sink fn (null ⇒ none).
-
void *callback_ctx = nullptr¶
The sink’s caller-owned context.
-
target_key_t target_key¶
Local re-dispatch target (refcount share, not a copy).
-
target_binding_t binding = {}¶
The minted slot for that target, or unbound (#830).
-
remote_ptr_t remote¶
The shared, immutable cold half (#1442) — null for the plain in-process edge. A HOLDER, not a borrow: it keeps the record alive for the whole dispatch, which is what the owning string copies used to do.
-
inline std::string_view link() const noexcept¶
-
class edge_snapshot_t¶
The fixed-capacity stack buffer of edge_view_t dispatch views — the no-heap small-fan-out half of vertex_t::snapshot_edges.
The element storage is RAW (uninitialized) bytes: declaring one on the publish hot path costs nothing, and only the views actually snapshotted are placement-constructed (and destroyed). A default-constructed
std::array<edge_view_t, 8>here instead zeroed ~900 bytes of stack per publish — GCC lowers that to eightrep stosblocks whose microcode startup latency dominated single-subscriber fan-out (the post-v0.3.0 fan1 delivery regression). Non-copyable; reused via clear.Public Functions
-
edge_snapshot_t() noexcept = default¶
An empty snapshot; the element storage stays uninitialized (the point).
-
edge_snapshot_t(const edge_snapshot_t&) = delete¶
Non-copyable — a transient dispatch buffer, never a value.
-
edge_snapshot_t &operator=(const edge_snapshot_t&) = delete¶
Non-assignable — a transient dispatch buffer, never a value.
-
inline ~edge_snapshot_t()¶
Destroy the constructed views (only those actually snapshotted).
-
inline void push_back(edge_view_t v)¶
Placement-construct
vas the next view; the caller (the snapshot loop) keeps the count ≤ kCapacity.
-
inline void clear() noexcept¶
Destroy every constructed view; the buffer is reusable afterwards.
-
inline std::size_t size() const noexcept¶
The number of views constructed.
-
inline edge_view_t &operator[](std::size_t i) noexcept¶
The
i-thsnapshotted view (i< size).
-
inline const edge_view_t &operator[](std::size_t i) const noexcept¶
The
i-thsnapshotted view (i< size), const.
Public Static Attributes
-
static constexpr std::size_t kCapacity = kInlineFanout¶
The snapshot width: the build’s
tr::graph::kInlineFanout(mirrored asvertex_t::kInlineFanout).
-
edge_snapshot_t() noexcept = default¶
-
struct edge_latch_t¶
The dispatch snapshot’s WIDTH, pinned — the delivery path’s bandwidth (#1448).
snapshot_edgeswrites one of these per active edge on every fan-out,kInlineFanoutof them live on the publishing thread’s stack, and a wide fan-out streamsFthrough the overflow vector. #844’s mid ladder exists because that array outgrows L1 somewhere in the 128→1024 gap, so the width is a measured hot-path quantity and not a housekeeping detail. 160 B before #1448 (twostd::strings and twoview_ts inline), 48 B after.A transient-local durability latch (RFC-0004 §D / Q4): the LKV plus the freshly admitted edge’s dispatch view, both snapshotted atomically with the append.
value stays null when no latch fired (the subscriber requested no durability — RFC-0022 §3.A — or the producer holds no LKV yet).
Public Members
-
value_ref_t value¶
The latched LKV; empty ⇒ no latch.
-
edge_view_t edge¶
The admitted edge’s dispatch view.
-
value_ref_t value¶
Owner app fields¶
-
enum class tr::graph::app_access_t : std::uint8_t¶
Owner-declared REMOTE writability of one application property field (RFC-0010 §A.2): what a caller-attributed field write/read may do. The OWNER — a local, caller-less host API call — always reads and writes its own declared fields;
ro/woconstrain remote callers only.Values:
-
enumerator RO¶
Remote read only — a remote write has no surface (
SCHEMA_NOT_FOUND).
-
enumerator RW¶
Remote read + write (a write still passes the vertex WRITE gate).
-
enumerator WO¶
Remote write only — no read surface: a secret never mirrors back.
-
enumerator RO¶
-
struct app_field_t¶
One entry of a vertex’s field descriptor table (RFC-0010 §A.2/§B): declaration, remote-writability, self-description, and current value of ONE application field under
:settings.app.— one record, so the schema can never drift from the gate.The value and descriptor bytes are OPAQUE to the runtime (stored and served verbatim — the last store-verbatim control surface, now that
:aclre-encodes from its parsed projection, #907): dtype/range validation is the owner’s, in its apply seam (handlers_t::on_app_field_write) — the runtime validates only addressing (declared / undeclared, writability): one table lookup.Public Members
-
std::string name = {}¶
The field’s key below
settings.app.— a.-joined spelling of the field steps ("kp","wifi.ssid"); the runtime keys the joined string flat.
-
app_access_t access = app_access_t::RO¶
Owner-declared remote writability.
-
retention_t retention = access == app_access_t::WO ? retention_t::NONE : retention_t::LAST¶
What a write to this field retains (RFC-0028 §5.4). Defaults from the access — a
wofield is retention_t::NONE — its write reaches handlers_t::on_app_field_write and stores nothing, since nothing may read it back — and every other field retention_t::LAST. Awofield stores nothing whatever this says. Lands in the padding after access.
-
std::vector<std::byte> descriptor = {}¶
The §B.1 descriptor record members (dtype/unit/min/max/label…, concatenated child TLVs) served inside this field’s
:schemaentry VERBATIM, after the runtime-projectedaccessmember. Never parsed by the runtime.
-
std::vector<std::byte> value = {}¶
The field’s current TLV bytes, stored and served verbatim (§D). Empty ⇒ never written (reads
NOT_FOUND; omitted from container reads). An install MAY carry an initial value here; on a field that retains nothing it is dropped.
-
std::string name = {}¶
-
using tr::graph::app_field_static_t = app_field_slot_t¶
The install-time spelling of app_field_slot_t — the same type. Kept as a name because it reads better at an owner’s borrowed
vertex_policy_t::app_fieldsdeclaration, and because it is the spelling already in the wild (docs, integrations, firmware tables).
-
struct app_field_slot_t¶
One app-field DECLARATION (ADR-0058, class ②): view-shaped, owning nothing.
Unlike app_field_t this owns NOTHING:
nameanddescriptorare VIEWS. For an OWNING install they point into app_field_table_t::owned; for a BORROWED install (vertex_policy_t::app_fields) they point at the caller’s own storage, and the caller guarantees the pointed-to bytes — and the array holding these entries — outlive the vertex. Pass static storage (flash /.rodata), never a stack array or a soon-freed heap block. Either way the storage is immutable for the table’s lifetime, so the views stay valid. Declaration only: no initial value (write values after install via the field-write surface).This is ONE type serving both roles. It used to be two —
app_field_static_tfor the install-time shape andapp_field_slot_tfor the runtime’s copy of it — which were field-for-field identical, so a borrowed install spent an allocation and a copy converting between them. Unifying them lets a borrowed table be viewed in place (ADR-0058 erratum 1).Public Functions
-
inline constexpr bool retains_nothing() const noexcept¶
True iff a write to this field stores nothing: declared retention_t::NONE, or
wo(no read surface, so nothing to keep).
Public Members
-
std::string_view name¶
Field key below
settings.app.(§A.1).
-
app_access_t access = app_access_t::RO¶
Owner-declared remote writability.
-
retention_t retention = access == app_access_t::WO ? retention_t::NONE : retention_t::LAST¶
What a write to this field retains — app_field_t::retention, with the same access-derived default. One byte beside access, in the padding the span’s alignment already left, so the slot does not grow; a positional table spells it third (
{name, access, retention, descriptor}).
-
std::span<const std::byte> descriptor = {}¶
§B.1 descriptor bytes, served verbatim.
-
inline constexpr bool retains_nothing() const noexcept¶
-
struct app_field_group_t¶
The lazily-allocated APP-FIELD group of the extension block (ADR-0058 Step 2): the RFC-0010 descriptor table plus its owner apply seam, together.
on_app_field_writeco-occurs with the field table (it is the table’s apply seam), NOT with the vertex’s value seam — so it lives here, not in value_handlers_t. A vertex with no app fields and no apply seam keeps this group null and pays neither the table nor the 16 B hook. Allocated on the first of eithervertex_policy_t::app_fields(the table) or anon_app_field_writeat registration; guarded by the vertex mutex, insert-only (never freed before the vertex).Public Functions
-
inline explicit app_field_group_t(mem::block_source_t &src) noexcept¶
An empty group whose table draws from
src(#1778).
Public Members
-
app_field_table_t table¶
The view-slot descriptor table + lazy value store.
-
app_field_write_hook_t on_app_field_write¶
The owner apply seam (RFC-0010 §A.3): fires after a declared field write stored its bytes, OUTSIDE the vertex lock. Unset ⇒ bytes just store. Never fires for a write the field ADMISSION filter refused — that filter lives on the GRAPH (
tr::graph::graph_t::admissions_), NOT here: this group is carried by every app-field-bearing vertex, and a filter almost none of them install may not cost all of them a hook.
-
inline explicit app_field_group_t(mem::block_source_t &src) noexcept¶
-
struct app_field_table_t¶
A vertex’s RFC-0010 field descriptor table (ADR-0058): the immutable declaration (class ②) split from the per-vertex mutable values (class ③).
Both install overloads converge here. A borrowed declaration leaves owned null and points slots straight at the caller’s array — the declaration costs zero RAM, neither bytes nor slots (measured host-side: 392 B / 10 allocs per leaf versus 695 B / 17 for the owning install, against a 136 B bare leaf — the
vertex_app5_staticandvertex_app5gate rows). Erratum 1 is what removed the slot copy; an earlier revision of this comment still described it (592 B / 11) after the code had stopped doing it. The owning declaration packs the slot array and the runtime table’s name+descriptor bytes into owned — ONE allocation for the whole table — and points the slots into it. owned is never mutated or reallocated whileslotsreference it (a re-install replaces the whole table under the vertex mutex).Public Functions
-
inline explicit app_field_table_t(mem::block_source_t &s) noexcept¶
An empty table whose owned storage draws from
s(the vertex’s extension block’s source, #1778).
-
inline app_field_table_t(app_field_table_t &&o) noexcept¶
Take
o'sblocks;ois left an empty table over the same source.
-
inline app_field_table_t &operator=(app_field_table_t &&o) noexcept¶
Release this table’s blocks, then take
o's.
Public Members
-
std::span<const app_field_slot_t> slots = {}¶
Per-field declaration views, in owner install order. Empty ⇒ no table installed (the closed
ENOTTYdefault). Guarded by the vertex mutex.A SPAN, not a container: a borrowed install points it straight at the caller’s array and allocates nothing for the declaration, which is what ADR-0058 §Step 1.2 promised and did not deliver (it copied into a
std::vector— see erratum 1). An owning install points it at the head of owned. Stable across the table’s moves: a move hands over the block, never copies it, and a borrowed span points outside the table entirely.
-
mem::block_source_t *src¶
The source every owned block below came from (#1778).
-
std::byte *owned = nullptr¶
The owning install’s ONE block: the slot array, then each field’s name and descriptor bytes, which the slots view. Null for a borrowed install. Sized exactly at install and never resized: a re-install builds a whole new table and move-assigns it under the stripe lock. Raw rather than two core arrays so the table is 48 B, under the 56 B its
std::vectorpredecessor took (vertex_app5gate).
-
mem::bytes_t *values = nullptr¶
Class-③ per-field values,
slots.size()of them, index-aligned with slots — LAZILY allocated, null until the first write to a RETAINING field on this vertex (#389 pattern). A declared-but-never-written table, and one whose only writes went towo/ retention_t::NONE fields, costs zero value RAM (RFC-0028 §5.4).values[i]empty ⇒ field i unset.
-
inline explicit app_field_table_t(mem::block_source_t &s) noexcept¶
-
class borrowed_fields_t¶
The argument type of a BORROWED app-field install — a table the caller promises outlives the vertex, constrained at compile time to storage shaped like it does (ADR-0058 erratum 2).
Erratum 1 tightened the borrowed install’s contract from “the `name`/`descriptor` bytes
must outlive the vertex” to “**the array too**”. Because the parameter was a
std::span, which binds implicitly to any contiguous range, that tightening reached callers as a SILENT change: the same call kept compiling and started dangling. This type closes the common case of that trap. It converts implicitly from aT[N]or astd::array— the two spellings aconstexpr/statictable takes — and NOT from astd::vector, so the natural way to build a table dynamically (fill a vector, install it, return) is now a compile error at the call site rather than a use-after-free found later by a downstream test suite. Default-constructed ({}) is the empty table, which uninstalls.What it does NOT prove: that the storage is
static. A block-scopeT[N]binds exactly like a namespace-scope one — C++ cannot express “static storage duration” as a constraint on a parameter. It rejects the container/temporary class of mistake, not every lifetime mistake. A caller whose table really is runtime-sized (a binding mapping a foreign POD array into slots, e.g.) opts out through unchecked, whose name is the point: the lifetime promise moves to the caller, in writing, at the call site.Public Functions
-
constexpr borrowed_fields_t() noexcept = default¶
The empty table — installs nothing, uninstalls an existing one.
-
template<std::size_t N>
inline constexpr borrowed_fields_t(const app_field_static_t (&table)[N]) noexcept¶ Borrow a C array of declarations — the
static constexpr kFields[]spelling.- Parameters:
table – The caller’s array; it and the bytes it points at MUST outlive the vertex.
-
template<std::size_t N>
inline constexpr borrowed_fields_t(const std::array<app_field_static_t, N> &table) noexcept¶ Borrow a
std::arrayof declarations — same contract as the C-array form.- Parameters:
table – The caller’s array; it and the bytes it points at MUST outlive the vertex.
-
inline constexpr std::span<const app_field_static_t> slots() const noexcept¶
The borrowed slots, in owner install order.
-
inline constexpr bool empty() const noexcept¶
True when the table declares no fields — the uninstall case.
Public Static Functions
-
static inline constexpr borrowed_fields_t unchecked(std::span<const app_field_static_t> table) noexcept¶
Borrow an arbitrary span, asserting the lifetime by hand — the escape hatch for a table whose extent is only known at run time.
Use when the storage is genuinely long-lived but not array-shaped at the call site: a language binding filling a
.bssslot array from a foreign POD table, say. The spelling is deliberately unpleasant — it is the caller taking the promise the implicit constructors would otherwise have checked the shape of.- Parameters:
table – Slots that MUST outlive the vertex, along with the bytes they point at.
-
constexpr borrowed_fields_t() noexcept = default¶
Lock striping¶
-
struct vertex_stripe_t¶
One shared lock stripe: the mutex + condvar a SET of vertices ride (#361 §2), replacing a per-vertex
std::mutex+std::condition_variable.Why: the blocking primitives were the single largest per-vertex RAM cost on the MCU target — ESP-IDF pthreads lazily allocate a FreeRTOS mutex (~90 B) plus condvar state PER VERTEX on first touch, and the host paid 88 B of struct. The LKV read/write hot path takes no VERTEX lock (the atomic shared_ptr swap), so a stripe serializes only control-plane verbs (ring trim, edge mutation, ACL state, seq/notify) — cross-vertex contention is wiring-frequency, not per-publish.
awaitwaits on the stripe’s condvar with a PER-VERTEX predicate (write_seq_), so a collision costs a spurious wake + re-check, never a correctness change.Public Members
-
std::mutex m¶
Serializes the stripe’s vertices’ verbs.
-
std::atomic<int> waiters = {0}¶
Live
awaitwaiters on this stripe. Mutated only under m, but READ without it by a publish that never takes the lock at all (#555), so it is atomic: the waiterless publish skips the mutex, not just the condvar call that #370 skipped. Seevertex_t::storefor the ordering argument that makes the lock-free read safe against a lost wakeup.
-
std::mutex m¶
-
inline std::size_t tr::graph::vertex_stripe_index(const void *v) noexcept¶
The stripe slot of a pinned vertex address (ADR-0056/0057 — the address is a stable identity). Same vertex ⇒ same slot, always.
-
inline vertex_stripe_t &tr::graph::vertex_stripe_at(std::size_t idx) noexcept¶
The stripe table:
constinitwhere the platform’sstd::mutexis constexpr-constructible, so the per-verb lookup is a plain indexed load with NO function-local-static init-guard check on the hot path (#370). libstdc++ makes the ctor constexpr only when its gthreads port supports static mutex init (__GTHREAD_MUTEX_INIT) — ESP-IDF’s does NOT — and libc++’s always is; the fallback is a guarded function-local static (one predicted branch per verb — the MCU’s constraint is RAM, not that branch). The condvars live in a separate guarded table (vertex_stripe_cv) becausestd::condition_variablecan never be constant-initialized — only the cold await/wake paths reach it.The stripe at table slot
idx(guarded-static fallback: this platform’sstd::mutexhas no constexpr ctor, so the table cannot beconstinit).
See: path, views, status & errors, security & ACL, config, interface-map.