Class: RubyReactor::Web::API
- Inherits:
-
Roda
- Object
- Roda
- RubyReactor::Web::API
- 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
- .async_step_type(config) ⇒ Object
- .background_handoff_for(reactor_class) ⇒ Object
- .build_structure(reactor_class) ⇒ Object
- .determine_step_type(config) ⇒ Object
- .execution_evidence?(data) ⇒ Boolean
- .extract_inner_class(config, param_name) ⇒ Object
- .extract_retry_inputs(data) ⇒ Object
-
.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.
-
.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.
- .hydrate_composed_contexts(composed_contexts, reactor_class_name) ⇒ Object
- .hydrate_map_ref(ref_data, reactor_class_name) ⇒ Object
- .reactor_status(data) ⇒ Object
- .scan_limit(raw) ⇒ Object
-
.with_map_summaries(results, structure, context_id, reactor_class_name) ⇒ Object
The reactor's normalized
{ mode: :after|:before, step: }hand-off pair, or nil.
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
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 |