arizona_event_attrs (arizona v0.4.0)
View SourceThe az-* attributes a page can prove it renders, and how the proof reaches
the client.
The client delegates a DOM event type only for a name it has been told about, so every name must be proved to carry commands -- guessing would either leave a declared event dead or hand app data to the command interpreter. There are two proofs, one per moment the question is answerable:
- Compile time. The parse transform records per module the
az-*names whose values it can prove are commands (thearizona_az_attrsmodule attribute) and the modules it can take markup from (arizona_az_deps): the component modules its templates name literally, and every module it calls, imports, or takes afun Mod:F/Aof by a literal name -- a remote helper's template (other_mod:button()) carries that module's folded names in its statics, which no render re-proves.all/1walks that graph from a set of roots (a route's handler plus its layout modules), so the connect frame delivers a page's whole vocabulary up front -- including names sitting in branches nothing has rendered yet. A handler may extend the graph by declaring-arizona_az_deps([mod, ...]).itself; every instance of the attribute is unioned. - Render time. What compile time cannot prove, evaluation can: a dynamic
attribute value (or
?localattribute init) that classifies as an{arizona_effect, _}command names its attribute (observe_attr/1) wherearizona_evalevaluates it -- every live mount, render and diff, so a mount observes without building the document and a diff observes every value its ops ship -- and a?stateful/?statelessinstantiation names its module (observe_mod/1) -- covering a module bound at runtime (?stateful(?get(page), ...)), which the walk cannot follow. A live process arms the collector (arm/0) and drains it into every reply it emits (drain/0); the socket walks newly observed modules, dedups against what it already sent, and ships the delta on that same frame, so a name arrives with the first frame that can render the markup declaring it.
The collector is process-dictionary state, armed only inside a live process: the request-free SSR paths (HTTP render, static generation) never arm it, so observation there is a no-op.
The walk follows every literal callee a module records, so it loads any of
them not loaded yet -- running that module's -on_load, as its first call
would. Each module's names and edges are cached in persistent_term, keyed by
module and stamped with the loaded code's MD5: a hit costs a module_loaded
check, the MD5 read, and a lookup, where re-reading the attributes of a large
framework callee costs microseconds per module on every connect. The stamp is
what keeps the cache honest across a hot reload (a cache keyed by module alone
once went stale there and served a page's events short): new code has a new
MD5, so its first walk re-reads it and replaces the entry -- one
persistent_term write per module version. The compiler's MD5 covers code,
not attributes; the transform-recorded ones follow the code (a folded name is
a template static, an edge is a call or a component atom), and the dev
reloader drops the entry of every module it loads itself, so a dev edit to
only a hand-written -arizona_az_deps/-arizona_az_attrs is seen too. Any
other load path (a release upgrade) keeps the previous version's entry for
such an edit until the code itself changes or the VM restarts. (Under rebar3 shell the agent never reloads an attribute-only edit at all: it decides what
changed by the same MD5.)
A module that fails to load is cached as absent, so a dangling edge does not
cost a code-server round trip on every connect; that entry is ignored once
the module is loaded by any other means (a deploy, the dev reloader).
The per-socket dedup the socket keeps on
top of it (az_walked/az_sent) is a narrower memo with a narrower window:
a hot reload that adds a FOLDED command attribute (a literal builder call
bakes into the statics, so no dynamic slot ever re-proves it) leaves already
open sockets without the new name until they reconnect -- which the dev
watcher's reload forces anyway.
Known limits, all loud or documented rather than silent:
- A command that reaches a LAYOUT attribute opaquely can never be delivered
(layouts render once, at SSR; no frame re-renders one) -- the render warns
(
observe_attr/1's unarmed branch). A?statelesswith a runtime-bound module inside a layout is the same hole one level up;?statefulin a layout is already a render-time error. - A non-transport caller of
arizona_live:handle_event/4(orpatch/2) receives that reply's drained observations and they die with it: unlike ops, which the stale-snapshot mechanism re-emits on the next root diff, an unchanged value is never re-proven. In-repo the only such callers are the terminal transports, which have no DOM to delegate on. - A helper template reached through no literal edge -- a module bound at
runtime (
Mod:button()), a fun value,apply/3, or an intermediate module compiled without the Arizona headers (it carries noarizona_az_deps, so the walk stops there) -- delivers only the names a render re-proves, never its FOLDED ones. Declare the edge by hand with-arizona_az_deps([mod]).on the page (or the intermediate module). The recorded edges also over-approximate: every literal callee counts whether or not its markup renders, at the same per-module grain as a?statefulin an untaken branch.
Summary
Functions
Every compile-time-proven az-* attribute reachable from Roots, themselves
included.
Arms the render-time collector in the calling process. Until armed,
observe_attr/1 and observe_mod/1 are no-ops.
Disarms the render-time collector in the calling process, dropping what it held: a render that armed it only for itself leaves the process as it found it.
The observations collected since the last drain, clearing the collector.
{[], []} in an unarmed process.
Records that a dynamic az-* attribute rendered a command value -- the typed
proof compile time cannot see for an opaque expression. Any other name is
ignored, so callers can hand over every dynamic attribute name unfiltered.
observe_mod/1 for a stateless render callback: the armed check runs BEFORE
the fun_info module lookup, so an unarmed render (SSR, static generation)
pays one dictionary read and nothing else.
Records that a component module was instantiated, so the socket can walk it -- the only way a module bound at runtime ever reaches the graph.
Types
Functions
Every compile-time-proven az-* attribute reachable from Roots, themselves
included.
-spec arm() -> ok.
Arms the render-time collector in the calling process. Until armed,
observe_attr/1 and observe_mod/1 are no-ops.
-spec disarm() -> ok.
Disarms the render-time collector in the calling process, dropping what it held: a render that armed it only for itself leaves the process as it found it.
-spec drain() -> observed().
The observations collected since the last drain, clearing the collector.
{[], []} in an unarmed process.
-spec observe_attr(Name) -> ok when Name :: binary().
Records that a dynamic az-* attribute rendered a command value -- the typed
proof compile time cannot see for an opaque expression. Any other name is
ignored, so callers can hand over every dynamic attribute name unfiltered.
observe_mod/1 for a stateless render callback: the armed check runs BEFORE
the fun_info module lookup, so an unarmed render (SSR, static generation)
pays one dictionary read and nothing else.
-spec observe_mod(Mod) -> ok when Mod :: module().
Records that a component module was instantiated, so the socket can walk it -- the only way a module bound at runtime ever reaches the graph.