Module: TypedEAV::EventDispatcher
- Defined in:
- lib/typed_eav/event_dispatcher.rb
Overview
In-process event-dispatch hub for Value and Field after_commit lifecycle events. Implements the contract that Phase 04 versioning and Phase 07 materialized index both depend on.
## Contract surface
-
‘Config.on_value_change` / `Config.on_field_change` are PUBLIC single proc slots (nil-default), backed by ActiveSupport::Configurable. Users set them via `TypedEAV.configure { |c| c.on_value_change = ->(…) }`.
-
‘register_internal_value_change(callable)` / `register_internal_field_change(callable)` are FIRST-PARTY hooks for in-gem features (Phase 04 versioning, Phase 07 matview DDL regen). They are not private_class_method because Phase 04 lives in `TypedEAV::Versioning::*` and cannot reach a truly-private class method — the `register_internal_*` naming + this comment block signal first-party-only intent.
-
Internal subscribers fire FIRST, in registration order. User proc fires LAST. Phase 04 reserves slot 0 of ‘value_change_internals` by convention.
## Error policy (split, locked at 03-CONTEXT.md §User-callback error policy)
-
Internal subscribers: exceptions PROPAGATE (fail-closed). Versioning corruption must be loud — silent failure leaves typed_eav_value_versions inconsistent with the live row. Without propagation, Phase 04 bugs would be invisible until someone audited the version table.
-
User proc: rescued via ‘rescue StandardError`, logged via `Rails.logger.error`, and SWALLOWED. The Value/Field row is already committed by the time the after_commit fires, so re-raising here would surface a misleading “save failed” error to the caller — the save actually succeeded.
## Out of scope for this module
-
‘:rename` detection happens in `Field`’s after_commit callback (the model has direct access to ‘saved_change_to_attribute?(:name)`).
-
Orphan-Value handling (‘field.nil?` because the field row was destroyed in the same transaction) is filtered at the model layer, not here. The dispatcher receives a guaranteed-non-nil object.
See ‘.vbw-planning/phases/03-event-system/03-CONTEXT.md` for the locked design decisions this module implements.
Class Method Summary collapse
-
.dispatch_field_change(field, change_type) ⇒ Object
Dispatch a field lifecycle event.
-
.dispatch_value_change(value, change_type) ⇒ Object
Dispatch a value lifecycle event.
-
.field_change_internals ⇒ Object
Internal subscribers for Field lifecycle events.
-
.register_internal_field_change(callable) ⇒ Object
Register an in-gem field-change subscriber.
-
.register_internal_value_change(callable) ⇒ Object
Register an in-gem value-change subscriber.
-
.reset! ⇒ Object
Clear ONLY the internal-subscribers arrays.
-
.value_change_internals ⇒ Object
Internal subscribers for Value lifecycle events.
Class Method Details
.dispatch_field_change(field, change_type) ⇒ Object
Dispatch a field lifecycle event. Called from ‘Field#after_commit` in plan 03-02. Same internals-first / user-last ordering and same error policy split as `dispatch_value_change`.
Signature: ‘(field, change_type)` — TWO args, no context. Field changes are CRUD-on-config (admin operations on field definitions), not per-entity user actions, so thread context is less relevant. Asymmetry vs `dispatch_value_change` is intentional and locked. `change_type` is one of `:create | :update | :destroy | :rename`.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/typed_eav/event_dispatcher.rb', line 120 def dispatch_field_change(field, change_type) field_change_internals.each { |cb| cb.call(field, change_type) } user = TypedEAV::Config.on_field_change return unless user begin user.call(field, change_type) rescue StandardError => e Rails.logger.error( "[TypedEAV] on_field_change raised: #{e.class}: #{e.} " \ "(field_id=#{field.id} field_name=#{field.name} change_type=#{change_type})", ) end end |
.dispatch_value_change(value, change_type) ⇒ Object
Dispatch a value lifecycle event. Called from ‘Value#after_commit` in plan 03-02. Internals fire FIRST (raises propagate), then the user proc fires LAST (errors logged + swallowed).
Signature: ‘(value, change_type, TypedEAV.current_context)` for both internals and user proc — context is injected here, not by callers. `change_type` is one of `:create | :update | :destroy`.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/typed_eav/event_dispatcher.rb', line 88 def dispatch_value_change(value, change_type) context = TypedEAV.current_context # Internals fire first, in registration order. Exceptions propagate — # versioning failure (Phase 04) must surface, never be silent. value_change_internals.each { |cb| cb.call(value, change_type, context) } user = TypedEAV::Config.on_value_change return unless user # User proc fires last. Wrapped in rescue because the Value row is # already committed — re-raising would surface a misleading "save # failed" error to the caller. Internal-vs-user error policy split # is locked at 03-CONTEXT.md §User-callback error policy. begin user.call(value, change_type, context) rescue StandardError => e Rails.logger.error( "[TypedEAV] on_value_change raised: #{e.class}: #{e.} " \ "(value_id=#{value.id} field_id=#{value.field_id} change_type=#{change_type})", ) end end |
.field_change_internals ⇒ Object
Internal subscribers for Field lifecycle events. Same registration protocol as ‘value_change_internals`.
56 57 58 |
# File 'lib/typed_eav/event_dispatcher.rb', line 56 def field_change_internals @field_change_internals ||= [] end |
.register_internal_field_change(callable) ⇒ Object
Register an in-gem field-change subscriber. Same first-party-only contract as ‘register_internal_value_change`. Field subscribers are invoked with `(field, change_type)` — TWO args, no context. The asymmetry vs value-change is locked at 03-CONTEXT.md §Phase Boundary.
77 78 79 |
# File 'lib/typed_eav/event_dispatcher.rb', line 77 def register_internal_field_change(callable) field_change_internals << callable end |
.register_internal_value_change(callable) ⇒ Object
Register an in-gem value-change subscriber. Called at engine boot by Phase 04 versioning and Phase 07 matview. Subscribers are invoked in registration order with ‘(value, change_type, context)`. Exceptions raised here PROPAGATE — fail-closed because versioning corruption must be loud. See module-level comment §“Error policy”.
NOT private_class_method: Phase 04 lives in TypedEAV::Versioning::* and cannot call a truly-private class method. The ‘register_internal_*` naming + this comment signal first-party-only intent.
69 70 71 |
# File 'lib/typed_eav/event_dispatcher.rb', line 69 def register_internal_value_change(callable) value_change_internals << callable end |
.reset! ⇒ Object
Clear ONLY the internal-subscribers arrays. Does NOT touch ‘Config.on_value_change` / `Config.on_field_change` — `Config.reset!` owns the user-proc state.
Splitting reset is load-bearing: Phase 04 versioning registers on the internal list at engine load. Calling ‘EventDispatcher.reset!` must NOT require re-running engine load to restore versioning. Test teardown that needs to clear EVERYTHING calls Config.reset! AND EventDispatcher.reset! — see 03-CONTEXT.md §“Reset split”.
145 146 147 148 |
# File 'lib/typed_eav/event_dispatcher.rb', line 145 def reset! @value_change_internals = [] @field_change_internals = [] end |
.value_change_internals ⇒ Object
Internal subscribers for Value lifecycle events. Populated at engine boot by Phase 04 versioning (slot 0) and Phase 07 matview (subsequent slots). Exposed as a reader for test introspection — first-party registration goes through ‘register_internal_value_change`.
50 51 52 |
# File 'lib/typed_eav/event_dispatcher.rb', line 50 def value_change_internals @value_change_internals ||= [] end |