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. It carries public callbacks and generic in-gem observers; durable version rows are written by transactional Value callbacks.
Contract surface
Config.on_value_change/Config.on_field_changeare PUBLIC single proc slots (nil-default), backed by ActiveSupport::Configurable. Users set them viaTypedEAV.configure { |c| c.on_value_change = ->(...) }.register_internal_value_change(callable)/register_internal_field_change(callable)are FIRST-PARTY hooks for in-gem observers. The explicit registration names signal their intended scope.- Internal subscribers fire FIRST, in registration order. User proc fires LAST. Durable ValueVersion writing is installed on Value transactions; this dispatcher does not own that write.
Error policy (split, locked at 03-CONTEXT.md §User-callback error policy)
- Internal observers: exceptions PROPAGATE (fail-closed).
- User proc: rescued via
rescue StandardError, logged viaRails.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
:renamedetection happens inField's after_commit callback (the model has direct access tosaved_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 observer.
-
.reset! ⇒ Object
Clear ONLY the internal-subscribers arrays.
-
.value_change_internals ⇒ Object
Internal subscribers for Value lifecycle observers.
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.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/typed_eav/event_dispatcher.rb', line 112 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.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/typed_eav/event_dispatcher.rb', line 79 def dispatch_value_change(value, change_type) context = TypedEAV.current_context # Internals fire first, in registration order. Exceptions propagate. value_change_internals.each do |cb| cb.call(value, change_type, context) end 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.
50 51 52 |
# File 'lib/typed_eav/event_dispatcher.rb', line 50 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.
68 69 70 |
# File 'lib/typed_eav/event_dispatcher.rb', line 68 def register_internal_field_change(callable) field_change_internals << callable end |
.register_internal_value_change(callable) ⇒ Object
Register an in-gem value-change observer. Observers are invoked in
registration order with (value, change_type, context). Exceptions
raised here PROPAGATE. See module-level comment §"Error policy".
NOT private_class_method: first-party code uses this named seam, and
the register_internal_* naming signals its intended scope.
60 61 62 |
# File 'lib/typed_eav/event_dispatcher.rb', line 60 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.
Transactional versioning callbacks are independently boot-latched on Value; resetting this dispatcher only resets observer registrations. Test teardown that needs to clear EVERYTHING calls Config.reset! AND EventDispatcher.reset!.
136 137 138 139 |
# File 'lib/typed_eav/event_dispatcher.rb', line 136 def reset! @value_change_internals = [] @field_change_internals = [] end |
.value_change_internals ⇒ Object
Internal subscribers for Value lifecycle observers. Exposed as a
reader for test introspection — first-party
registration goes through register_internal_value_change.
44 45 46 |
# File 'lib/typed_eav/event_dispatcher.rb', line 44 def value_change_internals @value_change_internals ||= [] end |