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

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