A STREAM vertex and its bounded history ring (L4 graph)

The STORED_VALUE contract is “the latest”: k writes leave one value. A STREAM vertex additionally appends each write to a bounded ring, and its contract is “observe every buffered entry” — which is why its propagation is a drain of the entries appended since the previous flush, not a coalesced last-writer-wins flush (reference 02 §Stream drain semantics).

What to notice

  • The depth has no wire surface at all. It is a retention intent only the application can supply, so it is declared owner-side with vertex_policy_t::retention — not a :settings knob. The withdrawn history_keep_last answers SCHEMA_NOT_FOUND on read and on write, caller-independently (RFC-0022 §3.B / §3.C).

  • Nothing is inherited. A declaration reaches exactly the vertex it names — no ancestor walk, no subtree push, no propagation question when a parent is reconfigured after its children exist (§3.F).

  • The ring is drain-only, and indexing never reaches it. [n] counts children of the stored value, never entries of the history ring (CONTEXT.md §Element addressing). The example reads the ring through history() and separately checks that a plain read still serves the latest value.

  • Overflow is not silent, in general. Shedding under pressure carries the flow-gap signal and is accounted for (RFC-0025 §4.5). What this example shows is the ordinary trim to the declared depth on a local producer, which is the retention intent doing its job — not a receiver-visible gap.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — a STREAM vertex keeps a bounded history ring, depth declared owner-side.
 9 *
10 * A `STORED_VALUE` vertex is last-writer-wins: *k* writes leave one value. A `STREAM` vertex
11 * additionally appends each write to a bounded ring, and its contract is "observe every
12 * buffered entry" rather than "the latest" (`docs/reference/02-graph-model.md` §Stream drain
13 * semantics). The depth is a RETENTION INTENT only the application can supply, so it is an
14 * owner-side call with **no wire surface at all** — `vertex_policy_t::retention`
15 * (`retention_t::N`), not a
16 * `:settings` knob (RFC-0022 §3.C; the withdrawn `history_keep_last` reads `SCHEMA_NOT_FOUND`).
17 *
18 * A plain `read` is unchanged: it still serves the latest value, never the ring.
19 *
20 * Runs under ctest as `example_graph_stream`; returns non-zero on any failed check.
21 */
22
23#include <array>
24#include <cstdio>
25#include <cstring>
26#include <span>
27#include <string_view>
28
29#include "libtracer/tracer.hpp"
30
31namespace {
32
33using tr::graph::path_t;
34using tr::graph::role_t;
35
36/** @brief An owned one-segment view over @p text. */
37tr::view::view_t value_of(std::string_view text) {
38    return *tr::view::over_bytes(std::as_bytes(std::span<const char>(text.data(), text.size())));
39}
40
41/** @brief True iff @p r's single view holds @p text. */
42bool holds(const tr::graph::value_t& r, std::string_view text) {
43    return r.only().bytes().size() == text.size() &&
44           std::memcmp(r.only().bytes().data(), text.data(), text.size()) == 0;
45}
46
47/** @brief Report expectation @p what and record a failure on @p ok. */
48void check(bool& ok, bool cond, const char* what) {
49    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
50    ok = ok && cond;
51}
52
53}  // namespace
54
55int main() {
56    tr::graph::graph_t g;
57    bool ok = true;
58
59    const auto events = g.register_vertex(path_t("/dev/can0/rx"), role_t::STREAM);
60    (void)g.set_policy(events,
61                       {.retention = tr::graph::retention_t::N,
62                        .depth = 3});  // owner-side retention intent; no peer can read or write it
63
64    for (const char* frame : {"f1", "f2", "f3", "f4"}) (void)g.write(events, value_of(frame));
65
66    // `history` fills caller storage and allocates nothing: each slot is a refcount share of a
67    // retained block. A span as long as the declared depth always holds the whole ring.
68    std::array<tr::graph::value_ref_t, 3> ring;
69    const auto n = g.history(events, ring);
70    check(ok, n.has_value(), "a STREAM vertex serves its history ring");
71    std::printf("ring holds %zu of the 4 writes\n", n ? *n : 0u);
72    check(ok, n && *n == 3, "the ring is bounded at the declared depth (oldest trimmed)");
73    check(ok, n && holds(*ring.front(), "f2"), "history is oldest-first, and f1 was trimmed");
74    check(ok, n && holds(*ring.back(), "f4"), "history is newest-last");
75
76    const auto latest = g.read(events);
77    check(ok, latest && holds(**latest, "f4"),
78          "a plain read still serves the latest value, not the ring");
79    return ok ? 0 : 1;
80}

See also: graph module · vertex roles and aggregation · user data packing.