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 |