Module: TypedEAV::Versioning

Defined in:
lib/typed_eav/versioning.rb,
lib/typed_eav/versioning/subscriber.rb

Overview

Phase 04 versioning namespace. Houses the Subscriber that writes TypedEAV::ValueVersion rows in response to Value lifecycle events dispatched by EventDispatcher.

## Architecture

  • TypedEAV::Versioning::Subscriber.call(value, change_type, context) is conditionally registered with EventDispatcher.register_internal_value_change at engine boot via ‘TypedEAV::Versioning.register_if_enabled`, which is invoked from the `config.after_initialize` block in lib/typed_eav/engine.rb. When TypedEAV.config.versioning is false (default), the helper returns early — no callable is added to the dispatcher chain. When true, the subscriber registers and runs FIRST in the value- change subscriber chain (slot 0 by `after_initialize` block declaration order — Phase 07 will declare its matview block LATER in the same engine to keep matview at slot ≥ 1).

  • The subscriber is gated by TWO checks at call time (both must pass for a version row to be written):

    1. value.field is non-nil (orphan guard — Value's field_id may
       have been NULLed by Phase 02's ON DELETE SET NULL cascade).
    2. TypedEAV.registry.versioned?(value.entity_type) == true
       (per-entity opt-in via has_typed_eav versioned: true or
       include TypedEAV::Versioned).
    

    The ‘Config.versioning` master switch is NOT re-checked inside the callable — when false, the subscriber is never registered in the first place.

  • Errors raised by Subscriber.call PROPAGATE per the EventDispatcher internal-vs-user error policy (03-CONTEXT.md §User-callback error policy). Versioning corruption must be loud — silent failure leaves the audit log inconsistent with the live row.

## Public API surface

The subscriber itself is gem-internal — apps do not call it directly. The public API is:

- `TypedEAV.config.versioning = true` — master switch.
- `has_typed_eav versioned: true` (or `include TypedEAV::Versioned`) —
  per-entity opt-in.
- `TypedEAV.config.actor_resolver = -> { ... }` — actor identification.
- `TypedEAV.with_context(actor: ..., source: ...) { ... }` — request-
  scoped audit context.
- `Value#history` and `Value#revert_to(version)` (plan 04-03).

Defined Under Namespace

Modules: Subscriber

Class Method Summary collapse

Class Method Details

.register_if_enabledObject

Conditionally register the Subscriber with EventDispatcher’s internal value-change subscriber chain. Called by the engine’s ‘config.after_initialize` block (lib/typed_eav/engine.rb).

Extracted into a class method (not inlined inside the after_initialize block) for testability: specs can call this against a freshly-cleared ‘EventDispatcher.value_change_internals` to exercise both branches (versioning on/off) in-process, without booting the engine. The slot-0 regression spec (plan 04-03 P03) and the zero-overhead verification spec (this plan, subscriber_spec engine-boot block) both rely on this seam.

Idempotent — safe to call multiple times. Calling twice with versioning on results in exactly ONE entry in ‘EventDispatcher.value_change_internals`. The idempotency check uses `Array#include?` against `Subscriber.method(:call)`; `Method#==` compares receiver+name (semantic equality), so two fresh `Subscriber.method(:call)` instances compare equal even though they are different Method objects. The engine block runs this exactly once per boot in production; the idempotency guard protects future code paths that might re-invoke for any reason.

When ‘TypedEAV.config.versioning` is false (default), this method is a no-op: zero callable in `value_change_internals`, zero per-write dispatch cost. That is the locked CONTEXT line 17 contract.



85
86
87
88
89
90
91
92
# File 'lib/typed_eav/versioning.rb', line 85

def self.register_if_enabled
  return unless TypedEAV.config.versioning

  method_ref = TypedEAV::Versioning::Subscriber.method(:call)
  return if TypedEAV::EventDispatcher.value_change_internals.include?(method_ref)

  TypedEAV::EventDispatcher.register_internal_value_change(method_ref)
end