ReactiveDag.Node.KeyRule (reactive_dag v0.17.0-rc.52)

Copy Markdown View Source

THE propagation rule: how a change reaches a parent, decided by what the parent DECLARED. ReactiveDag.Drain calls this — there is nothing to configure.

Node records each node's key_rule in cell.meta; this reads it:

  • :all — any input change escalates to a whole-cell recompute (the drain turns this into "*"). For aggregate / cross-range (reduce) cells.
  • :identity — a changed input key maps to the same output key (pass through). For key-local (map) cells. The default.

A recompute_by unit lowers to the :group forms below, which are richer than either: they map a changed CHILD key to the parent UNIT it belongs to, so a fold reprices one group rather than escalating to the whole cell.

rule/3 takes the specific child input, not just the parent — so a node whose legs propagate differently (a change to the members leg passing keys through while a change to the fn leg escalates) is expressible without a bespoke module. That was the last thing a host wrote its own rule for.

Summary

Functions

rule/3 with the DIFF each changed key was marked with — both sides of the change, from whichever writer produced it.

rule/4 with the plan's OPTS — currently :tenant.

Functions

rule(arg1, child, changed)

rule(parent, child, changed, diffs)

@spec rule(ReactiveDag.Cell.t(), ReactiveDag.Cell.id(), [String.t()], %{
  required(String.t()) => map()
}) ::
  ReactiveDag.KeyRule.result()

rule/3 with the DIFF each changed key was marked with — both sides of the change, from whichever writer produced it.

A diff survives its row and names where it went, so a :group claim stays precise in the two cases a live lookup cannot handle at all:

  • the row was deleted — nothing to read, but the diff still names the unit it belonged to;
  • the row moved between units — the live row names only where it landed, and BOTH units need repricing.

There is no live read on this path. An earlier version carried only the prior side, so a move had to union the snapshot with a lookup of where the row landed — and that lookup was itself what degraded to :all on a delete.

Keys with no diff (a source-fed leaf has no Ash row behind it) fall back to rule/3, so the two paths coexist.

rule(parent, child, changed, diffs, opts)

@spec rule(
  ReactiveDag.Cell.t(),
  ReactiveDag.Cell.id(),
  [String.t()],
  %{required(String.t()) => map()},
  keyword()
) :: ReactiveDag.KeyRule.result()

rule/4 with the plan's OPTS — currently :tenant.

A :group claim resolves by READING the changed rows, and that read has to be scoped: a tenanted resource refuses an unscoped one outright, so :group propagation raised for any host running a graph per tenant. :identity and :all never read, which is why this went unnoticed.

A fifth arity rather than widening rule/4, for the reason rule/4 itself was added rather than widening rule/3: the seam is public and hosts implement it. ReactiveDag.Graph calls the widest arity a module exports.