Module: TypedEAV::Versioning::Subscriber
- Defined in:
- lib/typed_eav/versioning/subscriber.rb
Overview
The Phase 04 internal subscriber. Conditionally registered with EventDispatcher.register_internal_value_change at engine boot via ‘TypedEAV::Versioning.register_if_enabled`. When registered, runs at slot 0 of the value-change subscriber chain.
## Contract
‘call(value, change_type, context)` — called by EventDispatcher. Returns nil (return value is ignored by EventDispatcher; the method’s job is the side effect of writing a ValueVersion row).
Two-gate short-circuit (the master switch is enforced at registration time, NOT here — when off, this callable is never registered):
1. `value.field` is nil → return nil (orphan guard).
2. `TypedEAV.registry.versioned?(value.entity_type) != true` →
return nil.
## Why a class method (not a class-with-state)
The subscriber holds NO instance state — it’s a stateless function of (value, change_type, context, gem state). A module method is cheaper to register (single proc reference, no allocation per call) and easier to mock in specs (‘allow(Subscriber).to receive(:call)`). If future versions need per-call state (e.g., batching), the call body can construct an instance internally without API change.
## Snapshot logic
The before_value / after_value hashes are keyed by typed-column name (locked in 04-CONTEXT.md). For each column in ‘field.class.value_columns`:
- :create → after = value[col]; before key absent (empty hash).
- :update → before = value.attribute_before_last_save(col);
after = value[col].
- :destroy → before = value[col] (still in-memory on the
destroyed record per Phase 03 P04 live-validation);
after key absent.
Column names are stringified for jsonb storage so query patterns like ‘WHERE before_value->>’integer_value’ = ‘42’‘ work uniformly regardless of how the subscriber wrote them.
## Actor resolution
‘TypedEAV.config.actor_resolver&.call` returns an AR record, scalar, or nil. We coerce via the same `respond_to?(:id) ? .id.to_s : .to_s` pattern as lib/typed_eav.rb:239-243 (normalize_one). nil flows through as nil (the typed_eav_value_versions.changed_by column is nullable per 04-CONTEXT.md §“actor_resolver returning nil”).
Class Method Summary collapse
-
.call(value, change_type, context) ⇒ Object
Public entry point.
Class Method Details
.call(value, change_type, context) ⇒ Object
Public entry point. EventDispatcher calls this with the locked 3-arg signature ‘(value, change_type, context)`.
NOTE: there is NO ‘Config.versioning` gate here. The subscriber is only registered with EventDispatcher when `Config.versioning` was true at engine `config.after_initialize` time (see `TypedEAV::Versioning.register_if_enabled`, invoked from lib/typed_eav/engine.rb’s ‘config.after_initialize` block). If versioning is off, the subscriber is never registered and never reached. The remaining gates are:
1. field-presence (orphan guard — Value's field_id may have
been NULLed by Phase 02's ON DELETE SET NULL cascade).
2. per-entity opt-in (Registry.versioned?).
70 71 72 73 74 75 |
# File 'lib/typed_eav/versioning/subscriber.rb', line 70 def call(value, change_type, context) return unless value.field return unless TypedEAV.registry.versioned?(value.entity_type) write_version_row(value, change_type, context) end |