Class: RubyReactor::Web::API

Inherits:
Roda
  • Object
show all
Defined in:
lib/ruby_reactor/web/api.rb

Overview

rubocop:disable Metrics/BlockLength, Metrics/ClassLength

Constant Summary collapse

NESTED_STRUCTURE_KEYS =

Step type -> the argument holding the child reactor class to recurse into.

{
  "compose" => :composed_reactor_class,
  "map" => :mapped_reactor_class,
  "async_reactor" => :async_reactor_class
}.freeze

Class Method Summary collapse

Class Method Details

.async_step_type(config) ⇒ Object



274
275
276
277
278
279
280
# File 'lib/ruby_reactor/web/api.rb', line 274

def self.async_step_type(config)
  case config.async_dispatch
  when :step then "async_step"
  when :reactor then "async_reactor"
  else "step"
  end
end

.background_handoff_for(reactor_class) ⇒ Object



254
255
256
257
258
# File 'lib/ruby_reactor/web/api.rb', line 254

def self.background_handoff_for(reactor_class)
  return nil unless reactor_class.respond_to?(:background_handoff)

  reactor_class.background_handoff
end

.build_structure(reactor_class) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/ruby_reactor/web/api.rb', line 199

def self.build_structure(reactor_class)
  return {} unless reactor_class.respond_to?(:steps)

  steps_config = reactor_class.steps
  return {} unless steps_config.is_a?(Hash)

  # Use DependencyGraph to calculate dependencies effectively
  graph = RubyReactor::DependencyGraph.new
  steps_config.each_value { |config| graph.add_step(config) }

  steps_config.to_h do |name, config|
    type = determine_step_type(config)

    step_data = {
      name: name,
      type: type,
      depends_on: graph.dependencies[name]
    }

    # An async_reactor child is a full reactor, so the dashboard drills
    # into its own step graph exactly as it does for compose/map.
    inner_key = NESTED_STRUCTURE_KEYS[type]
    if inner_key && (inner_class = extract_inner_class(config, inner_key))
      step_data[:nested_structure] = build_structure(inner_class)
    end

    [name, step_data]
  end
end

.determine_step_type(config) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/ruby_reactor/web/api.rb', line 260

def self.determine_step_type(config)
  if config.respond_to?(:interrupt?) && config.interrupt?
    "interrupt"
  elsif config.arguments&.key?(:composed_reactor_class)
    "compose"
  elsif config.arguments&.key?(:mapped_reactor_class)
    "map"
  elsif config.respond_to?(:async_dispatch)
    async_step_type(config)
  else
    "step"
  end
end

.execution_evidence?(data) ⇒ Boolean

Returns:



180
181
182
183
# File 'lib/ruby_reactor/web/api.rb', line 180

def self.execution_evidence?(data)
  (data[:execution_trace] || []).any? ||
    (data[:intermediate_results] || {}).any?
end

.extract_inner_class(config, param_name) ⇒ Object



282
283
284
285
286
287
# File 'lib/ruby_reactor/web/api.rb', line 282

def self.extract_inner_class(config, param_name)
  val = config.arguments.dig(param_name, :source)
  val.is_a?(RubyReactor::Template::Value) ? val.value : nil
rescue StandardError
  nil
end

.extract_retry_inputs(data) ⇒ Object



185
186
187
188
189
190
# File 'lib/ruby_reactor/web/api.rb', line 185

def self.extract_retry_inputs(data)
  inputs = data[:inputs] || {}
  return {} unless inputs.is_a?(Hash)

  inputs.transform_keys(&:to_sym)
end

.hydrate_async_reactor_ref(ref_data) ⇒ Object

An async_reactor child is an ordinary addressable execution, so its state is just its own context row — the same lookup any reactor uses.



320
321
322
323
324
325
326
327
328
329
# File 'lib/ruby_reactor/web/api.rb', line 320

def self.hydrate_async_reactor_ref(ref_data)
  execution_id = ref_data[:execution_id] || ref_data["execution_id"]
  child_class = ref_data[:reactor_class_name] || ref_data["reactor_class_name"]
  return ref_data unless execution_id && child_class

  child = RubyReactor.configuration.storage_adapter.retrieve_context(execution_id, child_class)
  return ref_data unless child

  ref_data.merge("context" => child)
end

.hydrate_async_step_ref(ref_data, reactor_class_name) ⇒ Object

The reference lives on the parent's context; the outcome lives in the Step Result Record. Resolve it so the dashboard can show whether the unit is still dispatched or has landed, mirroring hydrate_map_ref.



305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/ruby_reactor/web/api.rb', line 305

def self.hydrate_async_step_ref(ref_data, reactor_class_name)
  context_id = ref_data[:context_id] || ref_data["context_id"]
  name = ref_data[:name] || ref_data["name"]
  return ref_data unless context_id && name

  record = RubyReactor.configuration.storage_adapter.retrieve_step_result(
    context_id, name, reactor_class_name
  )
  return ref_data unless record

  ref_data.merge("record" => record)
end

.hydrate_composed_contexts(composed_contexts, reactor_class_name) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/ruby_reactor/web/api.rb', line 289

def self.hydrate_composed_contexts(composed_contexts, reactor_class_name)
  return {} unless composed_contexts.is_a?(Hash)

  composed_contexts.transform_values do |value|
    case (value[:type] || value["type"]).to_s
    when "map_ref" then hydrate_map_ref(value, reactor_class_name)
    when "async_step_ref" then hydrate_async_step_ref(value, reactor_class_name)
    when "async_reactor_ref" then hydrate_async_reactor_ref(value)
    else value
    end
  end
end

.hydrate_map_ref(ref_data, reactor_class_name) ⇒ Object



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/ruby_reactor/web/api.rb', line 331

def self.hydrate_map_ref(ref_data, reactor_class_name)
  storage = RubyReactor.configuration.storage_adapter
  map_id = ref_data[:map_id] || ref_data["map_id"]

  # Use the specific element reactor class if available, otherwise fallback to parent
  target_reactor_class = ref_data[:element_reactor_class] ||
                         ref_data["element_reactor_class"] ||
                         reactor_class_name

  # 1. Check for specific failure (O(1))
  # Stored by ResultHandler when a map element fails
  failed_context_id = storage.retrieve_map_failed_context_id(map_id, reactor_class_name)

  target_context_id = if failed_context_id
                        failed_context_id
                      else
                        # 2. Fallback to representative sample (last element) (O(1))
                        # If no failure, the last element gives a good idea of progress/completion
                        target_id = storage.retrieve_map_element_context_id(map_id, reactor_class_name, index: -1)
                        target_id
                      end

  return ref_data unless target_context_id

  # Retrieve the actual context data for the target ID
  representative_data = storage.retrieve_context(target_context_id, target_reactor_class)

  return ref_data unless representative_data

  {
    "name" => ref_data["name"],
    "type" => "map_element",
    "context" => representative_data
  }
end

.reactor_status(data) ⇒ Object



170
171
172
173
174
175
176
177
178
# File 'lib/ruby_reactor/web/api.rb', line 170

def self.reactor_status(data)
  status = data[:status].to_s == "skipped" ? "halted" : data[:status].to_s
  return status if %w[failed paused completed running halted pending].include?(status)
  return "cancelled" if data[:cancelled]
  return "running" if data[:current_step]
  return "completed" if execution_evidence?(data)

  "pending"
end

.scan_limit(raw) ⇒ Object



163
164
165
166
167
168
# File 'lib/ruby_reactor/web/api.rb', line 163

def self.scan_limit(raw)
  limit = raw.to_i
  return 50 unless limit.positive?

  [limit, 500].min
end

.with_map_summaries(results, structure, context_id, reactor_class_name) ⇒ Object

The reactor's normalized { mode: :after|:before, step: } hand-off pair, or nil. Kept out of build_structure deliberately — that hash is keyed by step name and the dashboard iterates it, so a non-step key there would render as a phantom node. A map that fails never records a result on the parent, so the step that actually failed had nothing to show in the dashboard. The element results are still in storage — summarize them exactly as a completed map's, so the failed elements stay inspectable.



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/ruby_reactor/web/api.rb', line 237

def self.with_map_summaries(results, structure, context_id, reactor_class_name)
  storage = RubyReactor.configuration.storage_adapter
  return results unless storage.respond_to?(:count_map_results)

  results = results.dup
  structure.each do |step_name, config|
    next unless config[:type] == "map"
    next if results.key?(step_name)

    map_id = "#{context_id}:#{step_name}"
    next if storage.count_map_results(map_id, reactor_class_name).to_i.zero?

    results[step_name] = RubyReactor::Map::ResultEnumerator.new(map_id, reactor_class_name)
  end
  results
end