Status: accepted (Phase 1)
Every reactive node needs a dependency list and a subscriber list. These lists are rebuilt on every evaluation of every computed and effect, which makes them the single hottest data structure in the framework.
Set<Node> on both ends. Simple, deduplicates for free, O(1) add/delete.
But: a Set allocates internal storage, rehashes as it grows, iterates with
an allocated iterator, and cannot express “the same edge, re-validated” — the
dependency set must be cleared and refilled on each run, which produces
garbage proportional to edge count per evaluation.Set for small
counts and iterable without allocation, but removal is O(n), and the
“unsubscribe from the source” direction requires searching the source’s
subscriber array.Link objects shared by both directions, reused in order.
Each edge is one object participating in two intrusive lists: the
subscriber’s dependency list and the source’s subscriber list. On
re-evaluation the subscriber walks its existing dependency list in order; if
the next source read matches the next link’s dep, the link is reused
untouched. Divergence allocates from that point; the tail is unlinked.Option 3, with version counters for deduplication within a single evaluation pass. The common case — a computed or effect reading the same sources in the same order — reuses every link and allocates nothing.
Set entry. Measured in the
core micro-benchmark; the allocation saving dominates at realistic graph
sizes. If a future measurement contradicts this, the structure is replaceable
behind the internal graph API.One object of 6 pointers plus a header per live edge, versus a Set entry plus
the Set’s own load-factor overhead on both ends. Dead edges are unlinked
eagerly, so a disposed subtree drops to zero retained edges immediately rather
than waiting for a GC cycle to notice a cleared Set.
None — this structure is entirely internal. It does constrain the internal code style: graph functions are written as flat pointer walks, not as iterator pipelines, which is less pleasant to read and is compensated with comments and exhaustive unit tests.
Set (allocation and rehash per evaluation), arrays (O(n) removal and
back-reference search), and “never unsubscribe, filter on notify” (unbounded
memory growth in long-lived apps).