Module: TypedEAV::Versioning

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

Overview

Transactional versioning namespace. Houses the Subscriber that writes TypedEAV::ValueVersion rows from Value lifecycle callbacks.

Architecture

  • TypedEAV::Versioning::Subscriber.call(value, change_type, context) is conditionally installed on Value's transactional callbacks at engine boot via TypedEAV::Versioning.register_if_enabled. The version row therefore shares the source transaction.

  • 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. 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

Constant Summary collapse

CALLBACKS =
{
  create: %i[_write_version_create after].freeze,
  update: %i[_write_version_update after].freeze,
  destroy: %i[_write_version_destroy before].freeze,
}.freeze

Class Method Summary collapse

Class Method Details

.atomic_callbacks_installed?Boolean

Returns:

  • (Boolean)


104
105
106
107
108
109
110
# File 'lib/typed_eav/versioning.rb', line 104

def self.atomic_callbacks_installed?
  CALLBACKS.all? do |event, (filter, kind)|
    TypedEAV::Value.send(:get_callbacks, event).to_a.any? do |callback|
      callback.filter == filter && callback.kind == kind
    end
  end
end

.register_if_enabledObject

Conditionally install the Subscriber on Value's transactional callbacks. Called by the engine's config.after_initialize block.

Extracted into a class method (not inlined inside the after_initialize block) for testability: specs can call this seam in-process without booting a second Rails application.

Idempotent — safe to call multiple times. Pool validation happens before any callback is installed, so a multi-database misconfiguration fails closed without partial activation.

When TypedEAV.config.versioning is false (default), this method is a no-op: no callback is installed and the disabled path adds no per-write predicate or dispatcher work.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/typed_eav/versioning.rb', line 71

def self.register_if_enabled
  return unless TypedEAV.config.versioning

  value_pool = TypedEAV::Value.connection_pool
  version_pool = TypedEAV::ValueVersion.connection_pool
  unless value_pool.equal?(version_pool)
    raise ArgumentError, "TypedEAV versioning requires Value and ValueVersion to share a connection pool"
  end

  callback_chains = callback_chains_for_installation

  callback_chains.each do |event, (filter, kind, chain)|
    next if chain.any? { |callback| callback.filter == filter && callback.kind == kind }

    TypedEAV::Value.set_callback(event, kind, filter, prepend: true)
  end
end