Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
node.hpp
Go to the documentation of this file.
1#pragma once
2
3// ============================================================================
4// reactive/node.hpp
5// ----------------------------------------------------------------------------
6// Defines `Node` -- the core abstraction of the reactive graph.
7//
8// In Aria, every entity that participates in dependency tracking
9// (Property, AutoComputed, Effect) embeds or inherits a `Node` and is
10// registered in the single process-wide DAG.
11//
12// Design highlights
13// -----------------
14// 1. Two-phase Push-Pull propagation. When a source changes, we only
15// recursively "color" (mark MaybeDirty) the downstream; actual
16// recomputation is deferred to batch-end / explicit flush. This is the
17// key ingredient that eliminates glitches.
18// 2. Monotonic `version` per node. When a downstream node pulls from an
19// upstream, it remembers the upstream's version. On the next pull it
20// compares again and skips recomputation if nothing actually changed.
21// 3. Intrusive doubly-linked edge lists. Each Edge is threaded into both
22// the source's "observers" list and the observer's "sources" list,
23// giving O(1) subscribe / unsubscribe with no heap allocation (the
24// Edge itself lives inside the observer or a dedicated pool).
25// 4. Single-thread assumption. All graph mutations must happen on the UI
26// thread. Debug builds assert on thread identity at public entry
27// points. Cross-thread updates must be marshalled via a Dispatcher.
28// ============================================================================
29
30#include "aria/abi/export.hpp"
31#include <cassert>
32#include <concepts>
33#include <cstdint>
34#include <memory>
35#include <string>
36#include <thread>
37#include <vector>
38
39namespace aria::reactive {
40
41class Node; // forward
42class Graph; // forward
43struct Edge;
44
45namespace detail {
46
50class NodeHandle {
51 friend class ::aria::reactive::Node;
52public:
53 NodeHandle() noexcept = default;
54 explicit NodeHandle(Node* node) noexcept;
55 NodeHandle(const NodeHandle& other) noexcept : NodeHandle(other.node_) {}
56 NodeHandle(NodeHandle&& other) noexcept;
57 NodeHandle& operator=(const NodeHandle& other) noexcept;
58 NodeHandle& operator=(NodeHandle&& other) noexcept;
59 ~NodeHandle() { reset_(); }
60
61 [[nodiscard]] Node* get() const noexcept { return node_; }
62 [[nodiscard]] Node& operator*() const noexcept { return *node_; }
63 [[nodiscard]] Node* operator->() const noexcept { return node_; }
64 explicit operator bool() const noexcept { return node_ != nullptr; }
65 friend bool operator==(const NodeHandle& a, const NodeHandle& b) noexcept {
66 return a.node_ == b.node_;
67 }
68
69private:
70 void reset_() noexcept;
71 void take_(NodeHandle& other) noexcept;
72 Node* node_ = nullptr;
73 NodeHandle* previous_ = nullptr;
74 NodeHandle* next_ = nullptr;
75};
76
77} // namespace detail
78
79// ---------------------------------------------------------------------------
80// Node role:
81// - Source : an actively mutable root node (Property is a Source)
82// - Derivation : value is computed from upstreams via compute()
83// (AutoComputed is a Derivation)
84// - Reaction : pure side-effect node (Effect / bind callback). Its
85// value is not exposed; it only runs an action on change.
86// ---------------------------------------------------------------------------
87enum class NodeKind : std::uint8_t {
91};
92
93// ---------------------------------------------------------------------------
94// Node state during a flush cycle:
95// - Clean : cached value matches all upstreams; pull is a no-op.
96// - MaybeDirty : an ancestor may have changed; must verify upstream
97// versions on pull before deciding whether to recompute.
98// - Dirty : at least one upstream has definitely changed; must
99// recompute.
100// - Computing : currently running compute(); used to detect cycles.
101// ---------------------------------------------------------------------------
102enum class NodeState : std::uint8_t {
103 Clean = 0,
105 Dirty = 2,
107};
108
113struct Edge {
114 Node* source = nullptr;
115 Node* observer = nullptr;
116
119 Edge* next_observer = nullptr;
120 Edge* prev_observer = nullptr;
121
124 Edge* next_source = nullptr;
125 Edge* prev_source = nullptr;
126
130 std::uint64_t observed_version = 0;
131};
132
136class Node {
137 // Graph is the single orchestrator of push-color / pull-evaluate and
138 // therefore needs privileged access to Node's intrusive lists and
139 // lifecycle state. All other code must go through the public surface.
140 friend class Graph;
141 friend class detail::NodeHandle;
142
143public:
144 explicit Node(NodeKind kind) noexcept : kind_(kind) {}
145 virtual ~Node() noexcept;
146
147 Node(const Node&) = delete;
148 Node& operator=(const Node&) = delete;
149 Node(Node&&) = delete;
150 Node& operator=(Node&&) = delete;
151
152 // ---- Read-only accessors ---------------------------------------------
153 [[nodiscard]] NodeKind kind() const noexcept { return kind_; }
154 [[nodiscard]] NodeState state() const noexcept { return state_; }
155 [[nodiscard]] std::uint64_t version() const noexcept { return version_; }
156 [[nodiscard]] const std::string& debug_name() const noexcept { return debug_name_; }
157
158 void set_debug_name(std::string name) { debug_name_ = std::move(name); }
159
172 [[nodiscard]] const std::string& effective_debug_name() const {
173 if (!debug_name_.empty()) return debug_name_;
174 if (fallback_name_.empty()) {
175 const char* kind = nullptr;
176 switch (kind_) {
177 case NodeKind::Source: kind = "Source"; break;
178 case NodeKind::Derivation: kind = "Derivation"; break;
179 case NodeKind::Reaction: kind = "Reaction"; break;
180 }
181 fallback_name_ = std::string(kind ? kind : "Node")
182 + "#" + std::to_string(node_id_);
183 }
184 return fallback_name_;
185 }
186
188 static Graph& graph() noexcept;
189
190 // ---- Methods below are intended for Graph / derived classes. -
191 // ---- They are public to simplify the implementation, not as API. -
192
196 void notify_changed();
197
200 void mark_downstream_maybe_dirty();
201
204 void mark_dirty() noexcept { state_ = NodeState::Dirty; }
205
208 [[nodiscard]] std::uint32_t depth() const noexcept { return depth_; }
209 void set_depth(std::uint32_t d) noexcept { depth_ = d; }
210
216 virtual bool recompute() { return false; }
217
220 [[nodiscard]] virtual std::shared_ptr<Node> retain_for_recompute() noexcept {
221 return {};
222 }
223
224 // ---- Edge manipulation (intrusive linked list) -----------------------
225 void attach_as_observer_of(Node& source, Edge& edge) noexcept;
226 void detach_edge(Edge& edge) noexcept;
227
230 void clear_sources() noexcept;
231
233 template<class F>
234 void for_each_observer(F&& f) {
235 for (Edge* e = observers_head_; e != nullptr; e = e->next_observer) {
236 f(*e);
237 }
238 }
239
242 template<class F>
243 void for_each_source(F&& f) const {
244 for (const Edge* e = sources_head_; e != nullptr; e = e->next_source) {
245 f(*e);
246 }
247 }
248
249 [[nodiscard]] bool has_observers() const noexcept { return observers_head_ != nullptr; }
250 [[nodiscard]] bool has_sources() const noexcept { return sources_head_ != nullptr; }
251
252protected:
255 void retire_() noexcept;
256
257 // Derived classes call this when a recompute actually changes the value.
258 void bump_version_() noexcept { ++version_; }
259 void set_state_(NodeState s) noexcept { state_ = s; }
260
261private:
262 NodeKind kind_;
264 bool queued_ = false;
265 bool resolving_ = false;
266 std::uint32_t depth_ = 0;
267 std::uint64_t version_ = 1;
268
269 // Intrusive list heads. Could be made circular to drop the prev
270 // pointers; kept doubly-linked for clarity of implementation.
271 Edge* observers_head_ = nullptr;
272 Edge* sources_head_ = nullptr;
273 detail::NodeHandle* handles_head_ = nullptr;
274
275 std::string debug_name_;
276 mutable std::string fallback_name_;
277
280 std::uint64_t node_id_ = next_node_id_();
281
282 ARIA_ABI_API static std::uint64_t next_node_id_() noexcept;
283};
284
285inline detail::NodeHandle::NodeHandle(Node* node) noexcept : node_(node) {
286 if (!node_) return;
287 next_ = node_->handles_head_;
288 if (next_) next_->previous_ = this;
289 node_->handles_head_ = this;
290}
291
292inline void detail::NodeHandle::reset_() noexcept {
293 if (!node_) return;
294 if (previous_) previous_->next_ = next_;
295 else node_->handles_head_ = next_;
296 if (next_) next_->previous_ = previous_;
297 node_ = nullptr;
298 previous_ = next_ = nullptr;
299}
300
301inline void detail::NodeHandle::take_(NodeHandle& other) noexcept {
302 node_ = other.node_;
303 previous_ = other.previous_;
304 next_ = other.next_;
305 if (node_) {
306 if (previous_) previous_->next_ = this;
307 else node_->handles_head_ = this;
308 if (next_) next_->previous_ = this;
309 }
310 other.node_ = nullptr;
311 other.previous_ = other.next_ = nullptr;
312}
313
314inline detail::NodeHandle::NodeHandle(NodeHandle&& other) noexcept { take_(other); }
315
316inline detail::NodeHandle& detail::NodeHandle::operator=(const NodeHandle& other) noexcept {
317 if (this != &other) {
318 NodeHandle copy{other};
319 *this = std::move(copy);
320 }
321 return *this;
322}
323
324inline detail::NodeHandle& detail::NodeHandle::operator=(NodeHandle&& other) noexcept {
325 if (this != &other) {
326 reset_();
327 take_(other);
328 }
329 return *this;
330}
331
332} // namespace aria::reactive
333
334namespace aria {
335
343template<typename T>
344concept ReactiveNode = std::derived_from<T, ::aria::reactive::Node>;
345
346} // namespace aria
Process-wide singleton reactive graph (accessed via Node::graph()).
Definition graph.hpp:139
Common base for every node participating in the reactive graph.
Definition node.hpp:136
void bump_version_() noexcept
Definition node.hpp:258
const std::string & effective_debug_name() const
A non-empty debug label for diagnostic output.
Definition node.hpp:172
bool has_observers() const noexcept
Definition node.hpp:249
void set_debug_name(std::string name)
Definition node.hpp:158
std::uint64_t version() const noexcept
Definition node.hpp:155
void mark_dirty() noexcept
Escalate state to Dirty (used by Graph::pull after confirming an upstream has truly moved).
Definition node.hpp:204
virtual std::shared_ptr< Node > retain_for_recompute() noexcept
Reactions are shared-owned and may cancel themselves while running.
Definition node.hpp:220
void set_depth(std::uint32_t d) noexcept
Definition node.hpp:209
bool has_sources() const noexcept
Definition node.hpp:250
Node(NodeKind kind) noexcept
Definition node.hpp:144
const std::string & debug_name() const noexcept
Definition node.hpp:156
std::uint32_t depth() const noexcept
Topological depth used by flush ordering.
Definition node.hpp:208
void for_each_source(F &&f) const
Iterate the source list (used by a Derivation in pull() to compare each upstream's current version ag...
Definition node.hpp:243
void for_each_observer(F &&f)
Iterate the observer list (used by Graph to color downstream nodes).
Definition node.hpp:234
virtual bool recompute()
Recompute hook for Derivation / Reaction nodes.
Definition node.hpp:216
NodeState state() const noexcept
Definition node.hpp:154
void set_state_(NodeState s) noexcept
Definition node.hpp:259
NodeKind kind() const noexcept
Definition node.hpp:153
friend class Graph
Definition node.hpp:140
Type that participates in the reactive graph: inherits from aria::reactive::Node.
Definition node.hpp:344
#define ARIA_ABI_API
Definition export.hpp:21
Definition computed.hpp:60
NodeState
Definition node.hpp:102
@ Computing
Definition node.hpp:106
@ Clean
Definition node.hpp:103
@ MaybeDirty
Definition node.hpp:104
@ Dirty
Definition node.hpp:105
NodeKind
Definition node.hpp:87
@ Derivation
Definition node.hpp:89
@ Reaction
Definition node.hpp:90
@ Source
Definition node.hpp:88
Definition validation_key.hpp:110
A single dependency edge: (upstream source) -> (downstream observer).
Definition node.hpp:113
Edge * prev_source
Definition node.hpp:125
Node * observer
The downstream node that depends on it.
Definition node.hpp:115
Edge * prev_observer
Definition node.hpp:120
std::uint64_t observed_version
Upstream version observed at the moment this edge was established or last confirmed.
Definition node.hpp:130
Edge * next_observer
Thread in the source's "observers" list (from the source's point of view: "these are the nodes watchi...
Definition node.hpp:119
Edge * next_source
Thread in the observer's "sources" list (from the observer's point of view: "these are the nodes I de...
Definition node.hpp:124
Node * source
The upstream node being observed.
Definition node.hpp:114