Class: Hecks::Runtime::Dispatcher
- Inherits:
-
Object
- Object
- Hecks::Runtime::Dispatcher
- Defined in:
- lib/hecks/runtime/dispatcher.rb
Defined Under Namespace
Classes: Result
Constant Summary collapse
- MAX_REACTION_DEPTH =
5
Instance Attribute Summary collapse
-
#registry ⇒ Object
readonly
Returns the value of attribute registry.
Instance Method Summary collapse
- #dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args) ⇒ Object
-
#dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args) ⇒ Object
THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH — never the domain itself.
-
#dry_run?(verb, **args) ⇒ Boolean
"IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same pipeline #dispatch itself runs (arguments coerced, givens checked, mutations applied IN MEMORY, ensures checked against the settled result), except
step_save/step_emitnever run, and neither do policies or sagas afterward: nothing here is committed, so nothing should react to it. - #events ⇒ Object
-
#initialize(registry) ⇒ Dispatcher
constructor
A new instance of Dispatcher.
- #max_reaction_depth ⇒ Object
- #policy_dispatches ⇒ Object
- #query(verb, **args) ⇒ Object
- #reaction_depth_reached? ⇒ Boolean
- #reactions ⇒ Object
-
#reenter(verb, saga_correlation: nil, **args) ⇒ Object
A reaction is the SYSTEM acting, not the caller who happened to be on the stack when the triggering command ran — the ambient caller is cleared for the reaction's own dispatch, so a triggering caller's role can neither satisfy nor block a reaction command it has nothing to do with (Runtime::Caller.without).
-
#reference_query(verb, **args) ⇒ Object
The same ask, answered by the reference interpreter alone — never the bound adapter's native hook.
- #saga_dispatches ⇒ Object
- #sagas ⇒ Object
- #verbs ⇒ Object
Constructor Details
#initialize(registry) ⇒ Dispatcher
Returns a new instance of Dispatcher.
41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/hecks/runtime/dispatcher.rb', line 41 def initialize(registry) @registry = registry rules = CommandRules.new(registry) @commands = CommandInterpreter.new(registry, rules: rules) @port_ops = PortOperationInterpreter.new(registry, rules: rules) @entities = EntityInterpreter.new(registry, rules: rules) @queries = QueryInterpreter.new(registry) @read_models = ReadModelInterpreter.new(registry) @policies = PolicyInterpreter.new(registry, door: self) @sagas = SagaInterpreter.new(registry, door: self) end |
Instance Attribute Details
#registry ⇒ Object (readonly)
Returns the value of attribute registry.
39 40 41 |
# File 'lib/hecks/runtime/dispatcher.rb', line 39 def registry @registry end |
Instance Method Details
#dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/hecks/runtime/dispatcher.rb', line 62 def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args) domain, aggregate_name, command_name = parse(verb) aggregate = resolve_aggregate(domain, aggregate_name, verb) instance, announced, execution_plan, persistence_outcome = if command_name.include?(".") head, sub = command_name.split(".", 2) port = aggregate.port(head) # A PORT OPERATION, reached by the SAME verb shape an entity # command already uses ("Domain::Aggregate.Head.Rest") — ports # are checked first, so an aggregate that ever declared both a # port and an entity of the same name would resolve to the # port; no domain in this corpus does, and `dispatch_port`'s # own header already named this as an open wire-spelling # question this resolves, not silently avoids. No `instance` # comes back — nothing is hydrated or saved by a port # operation (`PortOperationInterpreter`'s own header) — so # `Result#id`/`#state` are nil-safe (above) for exactly this # path. if port operation = port.operation(sub) || raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation", port: head, operation: sub.inspect)) route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args) [nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil] else entity_depth = command_name.split(".").size - 1 route = Routing.envelope(to, entity_depth: entity_depth) @entities.call(domain, aggregate, command_name, legacy_args, route: route, with: with) end else command = aggregate.command(command_name) || raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command", aggregate: aggregate_name, command: command_name.inspect)) args = Routing.payload(command, with: with, legacy: legacy_args) route = Routing.envelope(to) @commands.call(domain, aggregate, command, args, saga_correlation, route: route) end # Correlation is SET AT CONSTRUCTION now, not merged on here — # it is part of the transaction, known from this method's own # argument before a single event exists. It used to be stamped # onto already-emitted events, which is what kept an event # mutable after it had happened. # # The ordering this note used to guard still holds, and more # simply: `SagaInterpreter#advance` runs on THIS domain's # `announced` events within this very call, and finds the # correlation already there because it was never absent. announced.each { |event| @policies.react(event, domain) } announced.each { |event| @sagas.advance(event, domain) } Result.new(verb: verb, instance: instance, events: announced, execution_plan: execution_plan, persistence_outcome: persistence_outcome) end |
#dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args) ⇒ Object
THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH — never the
domain itself. port_name/operation_name are separate arguments
rather than one packed verb string on purpose: there is no established
wire spelling for "domain, aggregate, port, operation" yet, and
inventing one is a bigger decision than this call needs to make.
No adapter-to-port binding lookup happens here — that is
Hecks.adapter's existing job (unchanged by this), and wiring "which
adapter may call this port" through is the next piece, not this one.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/hecks/runtime/dispatcher.rb', line 173 def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args) aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}") port = aggregate.port(port_name) || raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}") operation = port.operation(operation_name) || raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}") route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args) announced = @port_ops.call(domain, aggregate, operation, args, route: route) announced.each { |event| @policies.react(event, domain) } announced.each { |event| @sagas.advance(event, domain) } announced end |
#dry_run?(verb, **args) ⇒ Boolean
"IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same
pipeline #dispatch itself runs (arguments coerced, givens checked,
mutations applied IN MEMORY, ensures checked against the settled
result), except step_save/step_emit never run, and neither do
policies or sagas afterward: nothing here is committed, so nothing
should react to it. Built for exactly the shape a whole-board
postcondition needs to be tested against (a downstream project's
own chess domain, checking "does this move leave my own king in
check" — the alternative was dispatching a real, unrelated piece's
own move purely to trigger the check, which then had to avoid
interfering with the very position being tested).
RAISES THE SAME REFUSALS #dispatch does — a DomainRefusal
subclass propagates normally, so a caller checking "would this be
legal" writes the identical rescue clause a real dispatch already
needs; this returns true only when nothing was refused.
NEVER A PORT VERB — PortOperationInterpreter's own side effects
(an external gateway call, say) have no meaningful in-memory-only
form, so this refuses one outright rather than silently running
it for real, which "dry" would otherwise quietly lie about.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/hecks/runtime/dispatcher.rb', line 141 def dry_run?(verb, **args) domain, aggregate_name, command_name = parse(verb) aggregate = resolve_aggregate(domain, aggregate_name, verb) if command_name.include?(".") head, = command_name.split(".", 2) if aggregate.port(head) raise WiringError, "#{verb} names a port operation — dry_run? has no in-memory form for one, " \ "only for aggregate and entity commands" end @entities.call(domain, aggregate, command_name, args, dry_run: true) else command = aggregate.command(command_name) || raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command", aggregate: aggregate_name, command: command_name.inspect)) @commands.call(domain, aggregate, command, args, dry_run: true) end true end |
#events ⇒ Object
53 |
# File 'lib/hecks/runtime/dispatcher.rb', line 53 def events = @registry.event_log |
#max_reaction_depth ⇒ Object
268 |
# File 'lib/hecks/runtime/dispatcher.rb', line 268 def max_reaction_depth = MAX_REACTION_DEPTH |
#policy_dispatches ⇒ Object
59 |
# File 'lib/hecks/runtime/dispatcher.rb', line 59 def policy_dispatches = @registry.policy_dispatch_log |
#query(verb, **args) ⇒ Object
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/hecks/runtime/dispatcher.rb', line 226 def query(verb, **args) domain, query_name = verb.to_s.split(".", 2) if query_name && !domain.include?("::") bluebook = @registry.bluebook(domain) || raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb)) model = bluebook.read_model(query_name) || raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_read_model", domain: domain, query: query_name.inspect)) return @read_models.call(domain, model, args) end domain, aggregate_name, query_name = parse(verb) aggregate = resolve_aggregate(domain, aggregate_name, verb) @queries.call(domain, aggregate, query_name, args) end |
#reaction_depth_reached? ⇒ Boolean
267 |
# File 'lib/hecks/runtime/dispatcher.rb', line 267 def reaction_depth_reached? = @reaction_depth.to_i >= MAX_REACTION_DEPTH |
#reactions ⇒ Object
55 |
# File 'lib/hecks/runtime/dispatcher.rb', line 55 def reactions = @registry.reaction_log |
#reenter(verb, saga_correlation: nil, **args) ⇒ Object
A reaction is the SYSTEM acting, not the caller who happened to be on the stack when the triggering command ran — the ambient caller is cleared for the reaction's own dispatch, so a triggering caller's role can neither satisfy nor block a reaction command it has nothing to do with (Runtime::Caller.without).
259 260 261 262 263 264 265 |
# File 'lib/hecks/runtime/dispatcher.rb', line 259 def reenter(verb, saga_correlation: nil, **args) depth = @reaction_depth.to_i @reaction_depth = depth + 1 Caller.without { dispatch(verb, saga_correlation: saga_correlation, **args) } ensure @reaction_depth = depth end |
#reference_query(verb, **args) ⇒ Object
The same ask, answered by the reference interpreter alone — never the bound adapter's native hook. Read models have no reference twin, so only the aggregate-query form answers here; the fuzzer's query oracle diffs this against #query's answer.
247 248 249 250 251 252 |
# File 'lib/hecks/runtime/dispatcher.rb', line 247 def reference_query(verb, **args) domain, aggregate_name, query_name = parse(verb) aggregate = resolve_aggregate(domain, aggregate_name, verb) @queries.reference_call(domain, aggregate, query_name, args) end |
#saga_dispatches ⇒ Object
58 |
# File 'lib/hecks/runtime/dispatcher.rb', line 58 def saga_dispatches = @registry.saga_dispatch_log |
#sagas ⇒ Object
57 |
# File 'lib/hecks/runtime/dispatcher.rb', line 57 def sagas = @registry.saga_log |
#verbs ⇒ Object
60 |
# File 'lib/hecks/runtime/dispatcher.rb', line 60 def verbs = @registry.verbs |