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