Module: Phlex::Reactive::Component::Registry
- Defined in:
- lib/phlex/reactive/component/registry.rb
Overview
ONE inheritance semantic for every Component class-level registry (issue #115). Before this, the five registries hand-rolled two DIVERGENT patterns: reactive_record_key walked the superclass LIVE on every read, while reactive_actions/reactive_state_keys/reactive_collections/ reactive_computes snapshot-dup'd the parent on FIRST access — so a parent declaring an action after a subclass had been read was visible via record-key semantics but silently invisible via action semantics.
The unified semantic is the live one: resolve through the superclass at read time, memoized per class against a process-wide generation counter bumped on any registry write. Declarations happen at class-load (boot, dev reload), so post-boot reads are pure memo hits; a late declaration anywhere simply invalidates the memos lazily via the generation compare on next read.
The hot-path contract (issue #115 invariant, performance rule) ===
The generation check gates registry RESOLUTION only. The identity-side memos read on EVERY reactive_token render — @reactive_record_ivar and fast paths with NO per-read comparison. They are invalidated HERE, at write time: bump! sweeps them off the writing class and all its descendants (a declaration is rare and class-load-shaped; a render is not). That write-time sweep is also what fixes the pre-#115 split-brain where a parent's late reactive_record left a subclass's memoized ivar stale against the live reactive_record_key.
Storage layout (all on the component class itself, so a Zeitwerk reload's fresh class object starts clean and nothing global retains a reference to app classes):
* @reactive_own_* — the class's OWN declarations
* @reactive_registry_cache — kind => resolved value
* @reactive_registry_generation — the generation the cache was built at
Resolved values are fresh copies per class (merge/+), matching the pre-#115 mutability surface: mutating a returned collection never reaches an ancestor. Direct mutation of a returned registry is unsupported either way — it bypasses the generation bump, so the next declaration anywhere discards it. Declare through the DSL.
Constant Summary collapse
- OWN_IVARS =
The class ivar carrying each registry's OWN declarations. A frozen lookup table (not :"@reactive_own_#kind") so no read or write allocates an intermediate string.
{ actions: :@reactive_own_actions, state_keys: :@reactive_own_state_keys, collections: :@reactive_own_collections, computes: :@reactive_own_computes, record_key: :@reactive_own_record_key, # Form-field namespace — reactive_scope (issue #180). scope: :@reactive_own_scope, # Deferred reply segments — reactive_lazy (issue #165). lazy: :@reactive_own_lazy, # verify_authorized opt-out (issue #168): a scalar bare flag (skip the # WHOLE component) plus a list of named actions. skip_all: :@reactive_own_skip_all, skip_actions: :@reactive_own_skip_actions }.freeze
- HOT_PATH_MEMOS =
The identity-side hot-path memos swept by bump! (see the contract above). Owned by Component::Identity; invalidated here.
%i[@reactive_record_ivar @reactive_state_ivars].freeze
- WRITE_MUTEX =
Serializes generation bumps so two concurrent class definitions can't lose an increment (a resolution memoized between the two writes must see a THIRD generation, not the second one again). Reads stay bare — the resolution compare tolerates staleness (it just recomputes).
Mutex.new
- EMPTY_HASH =
Frozen empties for the no-declaration branches of resolve_* — the merge/+ still returns a fresh, per-class copy.
{}.freeze
- EMPTY_LIST =
[].freeze
Class Attribute Summary collapse
-
.generation ⇒ Object
readonly
The process-wide registry generation.
Class Method Summary collapse
-
.append(klass, kind, values) ⇒ Object
Append values to a List-shaped registry (state_keys).
-
.resolve_hash(klass, kind, reader) ⇒ Object
Hash-shaped: ancestors-first merge, nearest declaration winning per key — same key ordering Hash#merge gives the pre-#115 snapshot-dup (inherited entries keep their position, own keys append).
-
.resolve_list(klass, kind, reader) ⇒ Object
List-shaped: ancestors' entries first, own appended.
-
.resolve_scalar(klass, kind, reader) ⇒ Object
Scalar-shaped: own declaration if present (even one an ancestor would override), else the nearest ancestor's resolution, else nil.
-
.write_entry(klass, kind, key, value) ⇒ Object
Store
key => valuein a Hash-shaped registry (actions, collections, computes). -
.write_scalar(klass, kind, value) ⇒ Object
Set a Scalar-shaped registry (record_key).
Class Attribute Details
.generation ⇒ Object (readonly)
The process-wide registry generation. Monotonic; never reset (a reloaded class carries no cache ivars, so it rebuilds regardless).
84 85 86 |
# File 'lib/phlex/reactive/component/registry.rb', line 84 def generation @generation end |
Class Method Details
.append(klass, kind, values) ⇒ Object
Append values to a List-shaped registry (state_keys). Keeps the pre-#115 concat semantics verbatim: declaration order, duplicates included.
100 101 102 103 |
# File 'lib/phlex/reactive/component/registry.rb', line 100 def append(klass, kind, values) own!(klass, kind) { [] }.concat(values) bump!(klass) end |
.resolve_hash(klass, kind, reader) ⇒ Object
Hash-shaped: ancestors-first merge, nearest declaration winning per key — same key ordering Hash#merge gives the pre-#115 snapshot-dup (inherited entries keep their position, own keys append).
123 124 125 126 127 128 129 |
# File 'lib/phlex/reactive/component/registry.rb', line 123 def resolve_hash(klass, kind, reader) fetch(klass, kind) do inherited = inherited_value(klass, reader) own = own(klass, kind) (inherited || EMPTY_HASH).merge(own || EMPTY_HASH) end end |
.resolve_list(klass, kind, reader) ⇒ Object
List-shaped: ancestors' entries first, own appended.
132 133 134 135 136 137 138 |
# File 'lib/phlex/reactive/component/registry.rb', line 132 def resolve_list(klass, kind, reader) fetch(klass, kind) do inherited = inherited_value(klass, reader) own = own(klass, kind) (inherited || EMPTY_LIST) + (own || EMPTY_LIST) end end |
.resolve_scalar(klass, kind, reader) ⇒ Object
Scalar-shaped: own declaration if present (even one an ancestor would override), else the nearest ancestor's resolution, else nil.
142 143 144 145 146 147 148 149 150 |
# File 'lib/phlex/reactive/component/registry.rb', line 142 def resolve_scalar(klass, kind, reader) fetch(klass, kind) do if klass.instance_variable_defined?(OWN_IVARS.fetch(kind)) klass.instance_variable_get(OWN_IVARS.fetch(kind)) else inherited_value(klass, reader) end end end |
.write_entry(klass, kind, key, value) ⇒ Object
Store key => value in a Hash-shaped registry (actions,
collections, computes). Returns the value, like the Hash#[]= the
declarations used pre-#115.
91 92 93 94 95 |
# File 'lib/phlex/reactive/component/registry.rb', line 91 def write_entry(klass, kind, key, value) own!(klass, kind) { {} }[key] = value bump!(klass) value end |
.write_scalar(klass, kind, value) ⇒ Object
Set a Scalar-shaped registry (record_key). The nearest declaration up the ancestry wins at resolve time.
107 108 109 110 111 |
# File 'lib/phlex/reactive/component/registry.rb', line 107 def write_scalar(klass, kind, value) klass.instance_variable_set(OWN_IVARS.fetch(kind), value) bump!(klass) value end |