Class: CMDx::Callbacks

Inherits:
Object
  • Object
show all
Defined in:
lib/cmdx/callbacks.rb

Overview

Registry of lifecycle callbacks invoked by Runtime. Callbacks can be method names (Symbols dispatched via task.send), blocks/Procs (instance_exec'd on the task), or arbitrary #call objects.

Each registration may carry :if / :unless gates (Symbol, Proc, or any #call-able). Gates are evaluated against the task before the callback is invoked; non-passing gates skip the callback silently.

Constant Summary collapse

EVENTS =

Callback event names Runtime dispatches.

Set[
  :before_validation,
  :before_execution,
  :around_execution,
  :after_execution,
  :on_complete,
  :on_interrupted,
  :on_success,
  :on_skipped,
  :on_failed,
  :on_ok,
  :on_ko
].freeze
POST_WORK_EVENTS =

Events fired after around_execution (work, rollback) finishes. Runtime passes the finalized Signal as the second callback argument so hooks can read reason, cause, metadata, origin, and backtrace.

Set[
  :after_execution,
  :on_complete,
  :on_interrupted,
  :on_success,
  :on_skipped,
  :on_failed,
  :on_ok,
  :on_ko
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCallbacks

Returns a new instance of Callbacks.



44
45
46
# File 'lib/cmdx/callbacks.rb', line 44

def initialize
  @registry = {}
end

Instance Attribute Details

#registryObject (readonly)

Returns the value of attribute registry.



42
43
44
# File 'lib/cmdx/callbacks.rb', line 42

def registry
  @registry
end

Instance Method Details

#around(event, task, &body) { ... } ⇒ void

This method returns an undefined value.

Wraps block with every callback registered for event as a nested chain (outer-first by declaration order). Each callback receives a continuation it must invoke exactly once: Symbol callbacks get it as their block (use yield); Procs/blocks are instance_exec'd on the task with (task, continuation); arbitrary callables receive (task, continuation). Gates skip individual links silently while still running the body.

Parameters:

  • event (Symbol)
  • task (Task)
  • body (#call)

    inner continuation (Runtime lifecycle body)

Yields:

  • the innermost link — the lifecycle body to wrap

Raises:

  • (CallbackError)

    when a callback fails to invoke its continuation



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/cmdx/callbacks.rb', line 176

def around(event, task, &body)
  callbacks = registry[event]
  return yield if callbacks.nil? || callbacks.empty?

  callbacks.reverse_each.reduce(body) do |succ, (callable, options)|
    lambda do
      next succ.call unless Util.satisfied?(options[:if], options[:unless], task)

      called = false
      cont = lambda do
        called = true
        succ.call
      end

      invoke(callable, task, continuation: cont, &cont)

      called || raise(CallbackError, <<~MSG.chomp)
        #{event} callback did not invoke its continuation.
        See https://drexed.github.io/cmdx/callbacks/#around_execution-the-wrap-the-whole-thing-hook
      MSG
    end
  end.call
end

#countInteger

Returns total callbacks across all events.

Returns:

  • (Integer)

    total callbacks across all events



132
133
134
# File 'lib/cmdx/callbacks.rb', line 132

def count
  registry.each_value.sum(&:size)
end

#deregister(event, callable = nil) ⇒ Callbacks

Drops callbacks registered for event. With no callable, removes every callback for event. With a callable, removes only the entries whose callback matches callable by == (works for Symbol method names, classes/modules, and any callable held by reference). When the last entry for event is removed, the key itself is dropped.

Parameters:

  • event (Symbol)

    one of EVENTS

  • callable (Symbol, #call, nil) (defaults to: nil)

    optional specific callback to remove

Returns:

Raises:

  • (ArgumentError)

    when event is unknown



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/cmdx/callbacks.rb', line 97

def deregister(event, callable = nil)
  unless EVENTS.include?(event)
    raise ArgumentError, <<~MSG.chomp
      unknown callback event #{event.inspect}, must be one of #{EVENTS.to_a.inspect}.
      See https://drexed.github.io/cmdx/callbacks/#what-callbacks-exist
    MSG
  end

  if callable.nil?
    registry.delete(event)
  elsif (entries = registry[event])
    entries.reject! { |cb, _opts| cb == callable }
    registry.delete(event) if entries.empty?
  end

  self
end

#empty?Boolean

Returns:

  • (Boolean)


122
123
124
# File 'lib/cmdx/callbacks.rb', line 122

def empty?
  registry.empty?
end

#initialize_copy(source) ⇒ void

This method returns an undefined value.

Parameters:

  • source (Callbacks)

    registry to duplicate



50
51
52
# File 'lib/cmdx/callbacks.rb', line 50

def initialize_copy(source)
  @registry = source.registry.transform_values(&:dup)
end

#key?(event) ⇒ Boolean

Returns whether a callback is registered under name.

Parameters:

  • name (Symbol)

Returns:

  • (Boolean)

    whether a callback is registered under name



117
118
119
# File 'lib/cmdx/callbacks.rb', line 117

def key?(event)
  registry.key?(event)
end

#process(event, task, signal = nil) ⇒ void

This method returns an undefined value.

Fires each callback registered for event against task. Skips any callback whose :if/:unless gates fail.

Post-work events (POST_WORK_EVENTS) receive the finalized signal as their second argument (Symbol method, Proc, or #call). Pre-work events and around_execution omit it.

Parameters:

  • event (Symbol)
  • task (Task)
  • signal (Signal, nil) (defaults to: nil)

    finalized halt token for post-work events

Raises:

  • (ArgumentError)

    when a callback is neither a Symbol nor responds to #call



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/cmdx/callbacks.rb', line 148

def process(event, task, signal = nil)
  return if empty?

  callbacks = registry[event]
  return if callbacks.nil? || callbacks.empty?

  post_work = POST_WORK_EVENTS.include?(event)
  callbacks.each do |callable, options|
    next unless Util.satisfied?(options[:if], options[:unless], task)

    invoke(callable, task, signal: post_work ? signal : nil)
  end
end

#register(event, callable = nil, **options, &block) { ... } ⇒ Callbacks

Adds a callback for event.

Parameters:

  • event (Symbol)

    one of EVENTS

  • callable (Symbol, #call, nil) (defaults to: nil)

    method name or callable; pass either this or a block

  • block (#call, nil)

    callback body when callable is omitted

  • options (Hash{Symbol => Object})

Options Hash (**options):

  • :if (Symbol, Proc, #call)

    gate that must evaluate truthy

  • :unless (Symbol, Proc, #call)

    gate that must evaluate falsy

Yields:

  • the callback body

Returns:

Raises:

  • (ArgumentError)

    when both callable and a block are given, when the callback type is invalid, or when event is unknown



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/cmdx/callbacks.rb', line 66

def register(event, callable = nil, **options, &block)
  callback = callable || block

  if callable && block
    raise ArgumentError, "callback: provide either a callable or a block, not both"
  elsif !callback.is_a?(Symbol) && !callback.respond_to?(:call)
    raise ArgumentError, <<~MSG.chomp
      callback must be a Symbol or respond to #call (got #{callback.class}).
      See https://drexed.github.io/cmdx/callbacks/#how-do-i-register-one
    MSG
  elsif !EVENTS.include?(event)
    raise ArgumentError, <<~MSG.chomp
      unknown callback event #{event.inspect}, must be one of #{EVENTS.to_a.inspect}.
      See https://drexed.github.io/cmdx/callbacks/#what-callbacks-exist
    MSG
  end

  (registry[event] ||= []) << [callback, options.freeze]
  self
end

#sizeInteger

Returns number of distinct events with callbacks.

Returns:

  • (Integer)

    number of distinct events with callbacks



127
128
129
# File 'lib/cmdx/callbacks.rb', line 127

def size
  registry.size
end