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,
  # 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

Class Method Summary collapse

Class Attribute Details

.generationObject (readonly)

The process-wide registry generation. Monotonic; never reset (a reloaded class carries no cache ivars, so it rebuilds regardless).



82
83
84
# File 'lib/phlex/reactive/component/registry.rb', line 82

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.



98
99
100
101
# File 'lib/phlex/reactive/component/registry.rb', line 98

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).



121
122
123
124
125
126
127
# File 'lib/phlex/reactive/component/registry.rb', line 121

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.



130
131
132
133
134
135
136
# File 'lib/phlex/reactive/component/registry.rb', line 130

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.



140
141
142
143
144
145
146
147
148
# File 'lib/phlex/reactive/component/registry.rb', line 140

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.



89
90
91
92
93
# File 'lib/phlex/reactive/component/registry.rb', line 89

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.



105
106
107
108
109
# File 'lib/phlex/reactive/component/registry.rb', line 105

def write_scalar(klass, kind, value)
  klass.instance_variable_set(OWN_IVARS.fetch(kind), value)
  bump!(klass)
  value
end