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 entire data API is three calls — read / write / await — and
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).
Reads/writes of the last-known-value are lock-free.
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 (a bounded ring sized by
:settings.history_keep_last), or handler (your on_read/on_write — covering
computed, proxy, sink, live-MMIO patterns). The last-known-value slot is an
atomic<shared_ptr<const rope_t>> swap, so read/write never take the per-vertex
mutex; that mutex guards only the subscriber list, history, and the await waiter
accounting; a per-vertex condvar makes await block until the next write
([ADR-0015] in the repo).
Subscriptions are field-writes, not a verb ([ADR-0006]): subscribing is
writing a SUBSCRIBER TLV into :subscribers[]. On each write the dispatcher
clones the value to every subscriber’s target vertex and/or in-process callback,
with a dispatch-depth cap (32) that terminates in-process cycles. :schema reads
return a POINT descriptor.
Interface¶
enum class role_t { STORED_VALUE, STREAM, HANDLER };
struct settings_t { /* reliability, durability, history_keep_last, deadline_ns, … */ };
struct handlers_t { std::function<result_t<rope_t>()> on_read;
std::function<result_t<void>(const rope_t&)> on_write; };
class graph_t {
vertex_handle_t register_vertex(const path_t&, role_t, handlers_t={}, settings_t={}); // infallible (ADR-0056)
result_t<vertex_handle_t> try_register_vertex(const path_t&, role_t, handlers_t={}, settings_t={}); // runtime path
result_t<rope_t> read (vertex_handle_t) const; // atomic LKV load (rope; scalar = 1 link)
result_t<void> write(vertex_handle_t, rope_t); // store + fan-out (view_t → rope_t implicit)
result_t<rope_t> await(vertex_handle_t, std::chrono::nanoseconds); // blocks for next write
result_t<std::vector<rope_t>> history(vertex_handle_t) const; // stream window
result_t<void> subscribe(const path_t& src, const path_t& target); // re-dispatch
result_t<void> subscribe(const path_t& src, std::function<void(const rope_t&)>); // callback
result_t<void> write(vertex_handle_t, const field_path_t&, rope_t); // handle-based field-write
std::optional<vertex_handle_t> find(std::span<const std::byte> key) const; // resolve → handle
result_t<rope_t> read (const path_t&) const; // field tail → :schema, …
result_t<void> write(const path_t&, rope_t); // field tail → :subscribers[]/:settings.*
};
No strings on the hot path
The hot path is handle-typed (the spec’s rule, reference/10 §path-handle).
A path_t encodes the canonical PATH bytes once — the path_t(std::string_view)
constructor for a known-good literal (ADR-0054), 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.
// idiomatic: encode the path once (parse-once ctor), reuse the handle
path_t p("/x:settings.reliability"); // once — no *-deref (ADR-0054)
auto v = *g.find(p.key()); // once — find → std::optional<vertex_handle_t>
for (...) g.write(v, p.field(), reliable_tlv); // hot loop — zero strings
Write → 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 (lock-free)
G->>V: snapshot subscribers (brief lock)
G-->>S1: callback(clone) %% refcount bump
G-->>S2: write(target, clone) %% re-dispatch, depth-capped
Note over V: await waiters woken via condvar
G-->>P: OK
Benefits¶
Three primitives —
read/write/awaitplus field-writes cover pub/sub, QoS, and discovery; noconnect/subscribeAPI to mismatch.Lock-free reads — the LKV is an atomic pointer swap; readers never block on writers (validated race-free under TSan).
Zero-copy fan-out — N subscribers get N refcount clones of one
rope_t, not N copies.The value is the bytes — a vertex stores a
rope_t, so what it holds is exactly what goes on the wire.
API reference¶
The declarations below are pulled directly from the reference implementation’s headers
(core/include/libtracer/graph.hpp, vertex.hpp) by Doxygen — they cannot drift from the
code.
-
class graph_t¶
The L4 in-process graph runtime: the vertex map plus the whole data API (register / read / write / await / subscribe, ADR-0006).
Vertices are keyed on their canonical PATH-TLV payload bytes (docs/reference/02 §dispatch). 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
-
using child_factory_t = std::function<result_t<vertex_handle_t>(graph_t&, std::vector<std::byte> child_key, const wire::tlv_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, 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).
Public Functions
-
graph_t()¶
Construct an empty graph (registers the built-in
stored_valuechild type).
-
vertex_handle_t register_vertex(const path_t &path, role_t role, handlers_t handlers = {}, settings_t settings = {})¶
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 = {}, settings_t settings = {})¶
Register a vertex at
path— FALLIBLE (the runtime-path form of register_vertex).- 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 = {}, settings_t settings = {})¶
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.- Returns:
The pinned vertex_handle_t, or
PATH_IN_USEif the key is already registered.
-
void register_child_type(std::string 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). Not thread-safe against concurrent creation — call at setup, before frames flow (mirrors the delivery sink). The built-instored_valuetype is registered by the constructor.
-
result_t<rope_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 rope (ADR-0053 §6): a scalar is the single-link case; a consumer needing contiguous bytes calls
rope_t::only()(single-link, zero copy) orrope_t::materialize(). 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.
-
result_t<void> write(vertex_handle_t v, rope_t value, std::string_view caller = {})¶
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).
-
result_t<void> write(vertex_handle_t v, const field_path_t &field, rope_t value, std::string_view caller = {})¶
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.
-
result_t<void> assign(vertex_handle_t v, 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.
-
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).
-
void set_delivery_mode(vertex_handle_t v, delivery_mode_t mode)¶
Set v’s per-vertex propagation policy (RFC-0008 §C).
A wiring-time call (the “configure before frames flow” contract), like settings; maintains the sweep’s UNCONDITIONAL membership. Default (unset) is IF_NEWER.
-
result_t<rope_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.- Returns:
The stored value as a rope, or a
status_t(e.g.TIMEOUT).
-
result_t<rope_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 stored rope); otherwise serve:schema,:acl, or a single:subscribers[N]slot (the slot’s stored SUBSCRIBER view, zero-copy) as a single-link rope. For the whole-array:subscribers[]read use read_subscribers. Used by the FWD resolver.
-
result_t<std::vector<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::vector<rope_t>> history(vertex_handle_t v) const¶
Stream history, newest last (Stream role only) — each entry the stored rope value.
-
result_t<void> subscribe(const path_t &src, const path_t &target)¶
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[].
-
result_t<void> subscribe(const path_t &src, std::function<void(const rope_t&)> callback)¶
Subscribe
srcto an in-processcallback(sugar; fires inline on each delivery to src with a cloned view).Delivery is value-agnostic (RFC-0008): WHICH vertices a sweep propagates is the source vertex’s delivery_mode, not a per-edge policy. A
std::functioncannot 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.
-
void set_remote_delivery_sink(std::function<void(const remote_delivery_t&, const rope_t&)> sink)¶
Install the sink the producer fan-out hands each REMOTE subscriber’s delivery to (#136, RFC-0004 §D/§E.1).
Set once at wiring time by the transport plane (
tr::net::fwd_router_t) before frames flow; the sink fires on whatever thread calls write (outside the vertex lock), and on subscribe for a transient-local latch. L4 keeps it as an opaquestd::function, so the graph never depends on a transport. A null sink (the default) ⇒ remote slots are stored but never deliver. The value reaches the sink as a rope (ADR-0053 §6): a single-link value materializes zero-copy, a multi-link value is handed over as the rope it is.
-
void set_subject_resolver(subject_resolver_t resolver)¶
Install the pluggable subject resolver (ADR-0018) — the ACL enforcement switch.
No resolver (the default) ⇒ enforcement is DISABLED: every operation is allowed, exactly today’s behavior, and the hot path pays one null check. With a resolver installed, each gated operation maps its caller context through it and — when a subject token comes back — evaluates the target vertex’s effective ACL (own ACEs + ancestor ACEs carrying INHERIT, ADR-0020): allowed iff some non-expired ACE with a matching subject (or
"EVERYONE@") grants the operation’s right bit; a vertex whose effective ACL is empty stays open (enforcement is opt-in per vertex via ACL presence). Denial returns status_t::PERMISSION_DENIED (tr::access::deniedon the wire, RFC-0002).Set once at wiring time, before frames flow — read-only afterwards on the op paths, so no lock (the remote-sink / child-catalog contract).
-
result_t<void> subscribe_wire(vertex_handle_t v, view_t source_view, view_t return_route, std::string link)¶
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 (ifvis transient-local,settings.durability == 1, and holds a value, the LKV is latched to this subscriber — one synchronous sink call, RFC-0004 §D).
-
result_t<rope_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.deadline_ns,:subscribers[],:schema) is routed to the field surface.
-
result_t<void> write(const path_t &path, rope_t value)¶
Write by path — resolve the key once, then write(vertex_handle_t, rope_t,std::string_view).
-
result_t<rope_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).
-
const settings_t &settings(vertex_handle_t v) const noexcept¶
The QoS settings of the vertex
vnames (ADR-0056).The read accessor the opaque handle does not expose directly: a resolver that needs a per-vertex knob (e.g.
store_ref_min_bytes, ADR-0042 §3) queries it here instead of dereferencing the vertex. Wiring-stable — settings change only via:settingswrites.
-
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).
-
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.
-
using child_factory_t = std::function<result_t<vertex_handle_t>(graph_t&, std::vector<std::byte> child_key, const wire::tlv_t *config)>¶
-
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 is lock-free (an atomic shared_ptr swap); the mutex guards only the history ring, the subscriber list, and the await waiter accounting. Non-copyable.Public Functions
-
inline vertex_t(role_t role, path_key_t key, settings_t settings, handlers_t handlers)¶
Construct a vertex with its role, canonical key, QoS settings, and handlers.
-
inline const path_key_t &key() const noexcept¶
This vertex’s canonical PATH-payload key (the vertex-map key).
-
inline const settings_t &settings() const noexcept¶
This vertex’s QoS settings.
-
inline vertex_t(role_t role, path_key_t key, settings_t settings, handlers_t handlers)¶
-
enum class tr::graph::role_t¶
A vertex’s behavioral role (docs/reference/11 §roles).
Values:
-
enumerator STORED_VALUE¶
Role 1: last-writer-wins; holds the last-written value.
-
enumerator STREAM¶
Role 2: bounded history ring sized by
settings.history_keep_last.
-
enumerator HANDLER¶
Roles 3-7: user
on_read/on_writesupplies the behavior.
-
enumerator STORED_VALUE¶
See: path, views, interface-map.