hecks 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -31,6 +31,10 @@ module Hecks
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Walks a canonical expression's parsed nodes (the same AST
|
|
35
|
+
# Bluebook::Expression::Evaluator evaluates) and collects the
|
|
36
|
+
# dotted paths it reads — used by Analyzer to classify a
|
|
37
|
+
# given/ensures/invariant rule's dependencies without evaluating it.
|
|
34
38
|
module ExpressionReads
|
|
35
39
|
module_function
|
|
36
40
|
|
|
@@ -59,6 +63,13 @@ module Hecks
|
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
66
|
+
# Static, correctness-first dependency analysis for one command:
|
|
67
|
+
# walks its mutations, lifecycle transitions, and given/ensures/
|
|
68
|
+
# invariant rules to derive a Plan (read_set/write_set/
|
|
69
|
+
# complete_state?/state_independent?) describing what the command
|
|
70
|
+
# touches without executing it. `Analyzer.call` is what
|
|
71
|
+
# CommandInterpreter and EntityInterpreter both consult before
|
|
72
|
+
# choosing a dispatch strategy.
|
|
62
73
|
class Analyzer
|
|
63
74
|
STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
|
|
64
75
|
|
|
@@ -101,11 +112,11 @@ module Hecks
|
|
|
101
112
|
# projected field just as easily as one of its real attributes
|
|
102
113
|
# (`Banking::Withdrawal.Dispute`'s own `parent.account_customer_
|
|
103
114
|
# status`, ATMCard's projected field, is a real, live example).
|
|
104
|
-
if aggregate.respond_to?(:projected_fields)
|
|
105
|
-
aggregate.projected_fields.each { |field| @owner_fields << field.name }
|
|
106
|
-
end
|
|
115
|
+
aggregate.projected_fields.each { |field| @owner_fields << field.name } if aggregate.respond_to?(:projected_fields)
|
|
107
116
|
if root_aggregate.respond_to?(:projected_fields)
|
|
108
|
-
root_aggregate.projected_fields.each
|
|
117
|
+
root_aggregate.projected_fields.each do |field|
|
|
118
|
+
@root_owner_fields << field.name
|
|
119
|
+
end
|
|
109
120
|
end
|
|
110
121
|
@payload_fields = command.attributes.to_set(&:name)
|
|
111
122
|
@state_reads = Set.new
|
|
@@ -210,9 +221,7 @@ module Hecks
|
|
|
210
221
|
lifecycle = aggregate.lifecycle
|
|
211
222
|
return unless lifecycle
|
|
212
223
|
|
|
213
|
-
if command.from
|
|
214
|
-
state_reads << lifecycle.field
|
|
215
|
-
end
|
|
224
|
+
state_reads << lifecycle.field if command.from
|
|
216
225
|
|
|
217
226
|
return if lifecycle.transitions_for(command.hecks_name).empty?
|
|
218
227
|
|
|
@@ -223,8 +232,8 @@ module Hecks
|
|
|
223
232
|
def analyze_rules(rules, phase:)
|
|
224
233
|
rules.each do |rule|
|
|
225
234
|
ExpressionReads.paths(rule.canonical).each { |path| classify_path(path, phase) }
|
|
226
|
-
rescue ArgumentError =>
|
|
227
|
-
unresolved << "expression #{rule.canonical.inspect} could not be analyzed: #{
|
|
235
|
+
rescue ArgumentError => e
|
|
236
|
+
unresolved << "expression #{rule.canonical.inspect} could not be analyzed: #{e.message}"
|
|
228
237
|
end
|
|
229
238
|
end
|
|
230
239
|
|
|
@@ -249,7 +258,6 @@ module Hecks
|
|
|
249
258
|
name = head.to_sym
|
|
250
259
|
|
|
251
260
|
if name == :parent
|
|
252
|
-
parent_field = nested.to_s.split(".", 2).first
|
|
253
261
|
# `root_owner_fields` — NOT `owner_fields`. For an entity-owned
|
|
254
262
|
# command `owner_fields` is the ENTITY's own attribute set;
|
|
255
263
|
# `parent.X` always means the ROOT aggregate's own field, a
|
|
@@ -257,18 +265,9 @@ module Hecks
|
|
|
257
265
|
# above, has the full bug this fixes). Identical for a plain
|
|
258
266
|
# aggregate command, where root_aggregate defaults to aggregate
|
|
259
267
|
# itself and the two sets are the same set.
|
|
260
|
-
|
|
261
|
-
unresolved << "#{path} does not name parent aggregate state"
|
|
262
|
-
else
|
|
263
|
-
state_reads << parent_field.to_sym
|
|
264
|
-
end
|
|
268
|
+
resolve_nested_state_read!(path, nested, root_owner_fields, "does not name parent aggregate state")
|
|
265
269
|
elsif name == :old
|
|
266
|
-
|
|
267
|
-
if old_field.empty? || !owner_fields.include?(old_field.to_sym)
|
|
268
|
-
unresolved << "#{path} does not name prior aggregate state"
|
|
269
|
-
else
|
|
270
|
-
state_reads << old_field.to_sym
|
|
271
|
-
end
|
|
270
|
+
resolve_nested_state_read!(path, nested, owner_fields, "does not name prior aggregate state")
|
|
272
271
|
elsif payload_fields.include?(name)
|
|
273
272
|
payload_reads << name
|
|
274
273
|
elsif owner_fields.include?(name)
|
|
@@ -278,6 +277,19 @@ module Hecks
|
|
|
278
277
|
end
|
|
279
278
|
end
|
|
280
279
|
|
|
280
|
+
# Shared shape behind the `:parent`/`:old` branches above: read the
|
|
281
|
+
# nested field name, check it against the given owner field set
|
|
282
|
+
# (deliberately different sets for `parent`/`old` — see the caller),
|
|
283
|
+
# and either record it as a state read or refuse with `message`.
|
|
284
|
+
def resolve_nested_state_read!(path, nested, field_set, message)
|
|
285
|
+
field = nested.to_s.split(".", 2).first
|
|
286
|
+
if field.empty? || !field_set.include?(field.to_sym)
|
|
287
|
+
unresolved << "#{path} #{message}"
|
|
288
|
+
else
|
|
289
|
+
state_reads << field.to_sym
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
281
293
|
# A partial mutation must preserve every untouched field on the
|
|
282
294
|
# correctness path. Those prior values are real reads even when no rule
|
|
283
295
|
# names them. A deterministic write needs no preservation read.
|
|
@@ -9,10 +9,18 @@ require_relative "query_interpreter"
|
|
|
9
9
|
require_relative "read_model_interpreter"
|
|
10
10
|
require_relative "policy_interpreter"
|
|
11
11
|
require_relative "saga_interpreter"
|
|
12
|
+
require_relative "outbox"
|
|
12
13
|
require_relative "../naming"
|
|
13
14
|
|
|
14
15
|
module Hecks
|
|
15
16
|
module Runtime
|
|
17
|
+
# The runtime's single dispatch entry point for one booted registry:
|
|
18
|
+
# routes a "Domain::Aggregate.Command"-shaped verb to the right
|
|
19
|
+
# sub-interpreter (command, entity, port operation, query, read
|
|
20
|
+
# model), then runs every policy and saga reaction the resulting
|
|
21
|
+
# events trigger. Also the door reactions dispatch back through
|
|
22
|
+
# (#reenter), and the one place reaction-cascade depth is tracked, to
|
|
23
|
+
# keep a policy/saga loop from re-triggering itself without bound.
|
|
16
24
|
class Dispatcher
|
|
17
25
|
MAX_REACTION_DEPTH = 5
|
|
18
26
|
|
|
@@ -48,8 +56,19 @@ module Hecks
|
|
|
48
56
|
@read_models = ReadModelInterpreter.new(registry)
|
|
49
57
|
@policies = PolicyInterpreter.new(registry, door: self)
|
|
50
58
|
@sagas = SagaInterpreter.new(registry, door: self)
|
|
59
|
+
# THE RELAY IS THE REGISTRY'S, NOT THIS DISPATCHER'S — the
|
|
60
|
+
# interpreters enqueue through `@registry.outbox` from inside
|
|
61
|
+
# the save transaction, and this dispatcher drains through the
|
|
62
|
+
# same object, so there is exactly one relay per registry no
|
|
63
|
+
# matter how many dispatchers front it. What it borrows from
|
|
64
|
+
# here is the pair of interpreters a consumer runs through.
|
|
65
|
+
@registry.outbox.attach(policies: @policies, sagas: @sagas)
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
# `runtime.outbox.rows`, `.rows(status: "claimed")`, `.redrive!`,
|
|
69
|
+
# `.log` — see `Runtime::Outbox`.
|
|
70
|
+
def outbox = @registry.outbox
|
|
71
|
+
|
|
53
72
|
def events = @registry.event_log
|
|
54
73
|
|
|
55
74
|
def reactions = @registry.reaction_log
|
|
@@ -63,7 +82,7 @@ module Hecks
|
|
|
63
82
|
domain, aggregate_name, command_name = parse(verb)
|
|
64
83
|
aggregate = resolve_aggregate(domain, aggregate_name, verb)
|
|
65
84
|
|
|
66
|
-
instance, announced, execution_plan, persistence_outcome =
|
|
85
|
+
instance, announced, execution_plan, persistence_outcome, outbox_rows =
|
|
67
86
|
if command_name.include?(".")
|
|
68
87
|
head, sub = command_name.split(".", 2)
|
|
69
88
|
port = aggregate.port(head)
|
|
@@ -83,7 +102,7 @@ module Hecks
|
|
|
83
102
|
raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation",
|
|
84
103
|
port: head, operation: sub.inspect))
|
|
85
104
|
route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
|
|
86
|
-
[nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil]
|
|
105
|
+
[nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil, :enqueue]
|
|
87
106
|
else
|
|
88
107
|
entity_depth = command_name.split(".").size - 1
|
|
89
108
|
route = Routing.envelope(to, entity_depth: entity_depth)
|
|
@@ -109,14 +128,30 @@ module Hecks
|
|
|
109
128
|
# `announced` events within this very call, and finds the
|
|
110
129
|
# correlation already there because it was never absent.
|
|
111
130
|
|
|
112
|
-
announced
|
|
113
|
-
|
|
114
|
-
announced.each { |event| @sagas.advance(event, domain) }
|
|
131
|
+
react(announced, domain, aggregate, outbox_rows)
|
|
115
132
|
|
|
116
133
|
Result.new(verb: verb, instance: instance, events: announced,
|
|
117
134
|
execution_plan: execution_plan, persistence_outcome: persistence_outcome)
|
|
118
135
|
end
|
|
119
136
|
|
|
137
|
+
# EVERYTHING OWED BECAUSE `announced` COMMITTED — policies first,
|
|
138
|
+
# then sagas, the order this method always ran them in. The
|
|
139
|
+
# command/entity interpreters hand back the outbox rows they
|
|
140
|
+
# enqueued inside the save transaction (`Interpreting#
|
|
141
|
+
# run_dispatch_order`); a port operation saves nothing, so its
|
|
142
|
+
# rows are enqueued here, after the fact (`:enqueue`) — durable
|
|
143
|
+
# still, just not transactional with anything, because there is
|
|
144
|
+
# nothing for them to be transactional with. `nil` rows mean the
|
|
145
|
+
# repository has no outbox: react directly, exactly as before.
|
|
146
|
+
def react(announced, domain, aggregate, outbox_rows)
|
|
147
|
+
return if announced.empty?
|
|
148
|
+
|
|
149
|
+
repository = @registry.repository(domain, aggregate)
|
|
150
|
+
outbox_rows = outbox.enqueue(repository, announced, domain) if outbox_rows == :enqueue
|
|
151
|
+
outbox.deliver(outbox_rows, announced, domain, repository)
|
|
152
|
+
end
|
|
153
|
+
private :react
|
|
154
|
+
|
|
120
155
|
# "IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same
|
|
121
156
|
# pipeline #dispatch itself runs (arguments coerced, givens checked,
|
|
122
157
|
# mutations applied IN MEMORY, ensures checked against the settled
|
|
@@ -180,8 +215,7 @@ module Hecks
|
|
|
180
215
|
route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
|
|
181
216
|
announced = @port_ops.call(domain, aggregate, operation, args, route: route)
|
|
182
217
|
|
|
183
|
-
announced
|
|
184
|
-
announced.each { |event| @sagas.advance(event, domain) }
|
|
218
|
+
react(announced, domain, aggregate, :enqueue)
|
|
185
219
|
|
|
186
220
|
announced
|
|
187
221
|
end
|
|
@@ -63,6 +63,14 @@ module Hecks
|
|
|
63
63
|
# which element it means, so re-deriving `wants` from `args` would be
|
|
64
64
|
# redundant at best and wrong if `args` no longer carries that identity
|
|
65
65
|
# at all.
|
|
66
|
+
# Locate, then copy-before-mutate, in that order — see the "ONE
|
|
67
|
+
# LEVEL DEEPER" comment below on why the copy has to happen exactly
|
|
68
|
+
# where it does (aliasing the adapter's own record otherwise).
|
|
69
|
+
# Splitting resolution from the copy/write-back would separate two
|
|
70
|
+
# halves of one aliasing-safety invariant across method boundaries.
|
|
71
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
72
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
73
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
66
74
|
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
67
75
|
entity_name = entity.hecks_name
|
|
68
76
|
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
@@ -158,32 +166,43 @@ module Hecks
|
|
|
158
166
|
# method's own fixture (TaggedList.Bump, a VO-typed `count` with
|
|
159
167
|
# `default: 0`) raised exactly this TypeMismatch on its first
|
|
160
168
|
# real run.
|
|
161
|
-
|
|
169
|
+
# A case dispatching over a closed, declared set of mutation ops —
|
|
170
|
+
# deliberately kept byte-for-byte parallel to its aggregate-level
|
|
171
|
+
# twin, MutationApplier#apply (see this method's own comment
|
|
172
|
+
# above and each branch's "own entity-scoped twin" cross-reference):
|
|
173
|
+
# extracting the near-duplicate increment/decrement/multiply
|
|
174
|
+
# shape here without doing the same there would break that
|
|
175
|
+
# intentional mirroring, which is what lets the two be diffed
|
|
176
|
+
# against each other when one gets a fix the other needs too.
|
|
177
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
178
|
+
# `pre` — the element as it was before this command (C4.2): every
|
|
179
|
+
# read below goes through it, every write lands on `element`.
|
|
180
|
+
def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
|
|
162
181
|
case mutation.op
|
|
163
182
|
when :set
|
|
164
183
|
value = rules.resolve_source(mutation.source, args)
|
|
165
184
|
attribute = entity.attribute(mutation.target)
|
|
166
185
|
element[mutation.target] = attribute ? Value.for_attribute(aggregate, attribute, value) : value
|
|
167
186
|
when :append
|
|
168
|
-
element[mutation.target] = appended_to_element(aggregate, entity,
|
|
187
|
+
element[mutation.target] = appended_to_element(aggregate, entity, pre, mutation, args)
|
|
169
188
|
when :remove
|
|
170
|
-
element[mutation.target] = removed_from_element(rules, aggregate, entity,
|
|
189
|
+
element[mutation.target] = removed_from_element(rules, aggregate, entity, pre, mutation, args)
|
|
171
190
|
when :increment, :decrement
|
|
172
191
|
attribute = entity.attribute(mutation.target)
|
|
173
192
|
amount = rules.resolve_source(mutation.source, args)
|
|
174
|
-
current =
|
|
193
|
+
current = pre[mutation.target]
|
|
175
194
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
176
195
|
result = rules.arithmetic(current, amount, mutation.target, rules.sign_of(mutation.op))
|
|
177
196
|
element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
178
197
|
when :multiply
|
|
179
198
|
attribute = entity.attribute(mutation.target)
|
|
180
199
|
amount = rules.resolve_source(mutation.source, args)
|
|
181
|
-
current =
|
|
200
|
+
current = pre[mutation.target]
|
|
182
201
|
amount = Value.for_attribute(aggregate, attribute, amount) if attribute && current.is_a?(Value)
|
|
183
202
|
result = rules.multiply(current, amount, mutation.target)
|
|
184
203
|
element[mutation.target] = rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
185
204
|
when :clamp
|
|
186
|
-
element[mutation.target] = rules.clamp(
|
|
205
|
+
element[mutation.target] = rules.clamp(pre[mutation.target], mutation.source, mutation.target)
|
|
187
206
|
else
|
|
188
207
|
# The aggregate-level twin's own backstop
|
|
189
208
|
# (MutationApplier#apply), for the same reason: applying
|
|
@@ -14,6 +14,13 @@ require_relative "command_interpreter/argument_gate"
|
|
|
14
14
|
|
|
15
15
|
module Hecks
|
|
16
16
|
module Runtime
|
|
17
|
+
# Interprets a command dispatched against a nested entity (a dotted
|
|
18
|
+
# verb like "Handler.Dispatch.Bind") rather than an aggregate root
|
|
19
|
+
# directly: walks the entity chain off the parent aggregate, applies
|
|
20
|
+
# the command's own DISPATCH_ORDER of steps against the located
|
|
21
|
+
# element, then saves and emits through the same parent record a
|
|
22
|
+
# plain aggregate command would. CommandInterpreter's sibling for
|
|
23
|
+
# entity-owned commands.
|
|
17
24
|
class EntityInterpreter
|
|
18
25
|
include Interpreting
|
|
19
26
|
# THE SAME PAYLOAD GATE aggregate commands and port operations already
|
|
@@ -61,9 +68,14 @@ module Hecks
|
|
|
61
68
|
# targets, so every step written before this ADR (enforce_givens,
|
|
62
69
|
# apply_mutations, advance_lifecycle, element_identity, ...) reads
|
|
63
70
|
# exactly as it always has. Only `locate_element` walks the chain.
|
|
71
|
+
# `:chain` shadows Enumerable#chain on purpose — every read is
|
|
72
|
+
# `ctx.chain` fetching the field (an Array of entities), never
|
|
73
|
+
# `ctx.chain(other)` combining enumerables. Verified before disabling
|
|
74
|
+
# this cop for it.
|
|
75
|
+
# rubocop:disable-next Lint/StructNewOverride
|
|
64
76
|
Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
|
|
65
77
|
:args, :repository, :instance, :chain, :element, :view, :transition,
|
|
66
|
-
:old_element, :result, :route, :plan, :persistence_outcome, :dry_run)
|
|
78
|
+
:old_element, :result, :route, :plan, :persistence_outcome, :dry_run, :outbox_rows)
|
|
67
79
|
|
|
68
80
|
def initialize(registry, rules:)
|
|
69
81
|
@registry = registry
|
|
@@ -111,7 +123,7 @@ module Hecks
|
|
|
111
123
|
ctx.repository = @registry.repository(domain, aggregate)
|
|
112
124
|
lock_id = Identity.best_effort(aggregate, args, route)
|
|
113
125
|
run_dispatch_order_with_isolation(DISPATCH_ORDER, ctx, lock_key_id: lock_id)
|
|
114
|
-
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
|
|
126
|
+
[ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome, ctx.outbox_rows]
|
|
115
127
|
rescue StaleWrite
|
|
116
128
|
attempt += 1
|
|
117
129
|
retry if attempt < MAX_STALE_WRITE_RETRIES
|
|
@@ -149,10 +161,10 @@ module Hecks
|
|
|
149
161
|
# `ArgumentGate#refuse_unknown_arguments`'s own header gives for `:id`/
|
|
150
162
|
# the root's `identity_heads`.
|
|
151
163
|
def step_refuse_unknown_arguments(ctx)
|
|
152
|
-
step(:refuse_unknown_arguments)
|
|
164
|
+
step(:refuse_unknown_arguments) do
|
|
153
165
|
refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.command, ctx.args,
|
|
154
166
|
extra_identity_heads: ctx.chain.flat_map(&:identity_heads))
|
|
155
|
-
|
|
167
|
+
end
|
|
156
168
|
end
|
|
157
169
|
|
|
158
170
|
# No `aggregate:` exemption to pass — that kwarg exists only for a
|
|
@@ -180,15 +192,15 @@ module Hecks
|
|
|
180
192
|
def step_hydrate_parent(ctx)
|
|
181
193
|
# `ctx.repository` is resolved once, in `#call`, before the
|
|
182
194
|
# isolation decision — not here any more.
|
|
183
|
-
ctx.instance = step(:hydrate_parent)
|
|
195
|
+
ctx.instance = step(:hydrate_parent) do
|
|
184
196
|
parent(ctx.repository, ctx.aggregate, ctx.entity_name, ctx.command_name, ctx.args, ctx.route)
|
|
185
|
-
|
|
197
|
+
end
|
|
186
198
|
end
|
|
187
199
|
|
|
188
200
|
def step_locate_element(ctx)
|
|
189
|
-
ctx.element = step(:locate_element)
|
|
201
|
+
ctx.element = step(:locate_element) do
|
|
190
202
|
EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
|
|
191
|
-
|
|
203
|
+
end
|
|
192
204
|
# `view` was hydrated ONCE, here, into its OWN state hash
|
|
193
205
|
# (Value.hydrate builds a fresh Hash — never aliased with `element`)
|
|
194
206
|
# — exactly right for enforce_givens, which must read pre-mutation.
|
|
@@ -197,9 +209,9 @@ module Hecks
|
|
|
197
209
|
end
|
|
198
210
|
|
|
199
211
|
def step_enforce_givens(ctx)
|
|
200
|
-
step(:enforce_givens)
|
|
212
|
+
step(:enforce_givens) do
|
|
201
213
|
@rules.enforce_givens(ctx.view, ctx.command, ctx.args, domain: ctx.domain, declaring: ctx.entity, parent: ctx.instance)
|
|
202
|
-
|
|
214
|
+
end
|
|
203
215
|
end
|
|
204
216
|
|
|
205
217
|
def step_admissible_transition(ctx)
|
|
@@ -208,11 +220,12 @@ module Hecks
|
|
|
208
220
|
|
|
209
221
|
def step_apply_mutations(ctx)
|
|
210
222
|
ctx.old_element = ctx.element.dup unless ctx.command.ensures.empty?
|
|
211
|
-
step(:apply_mutations)
|
|
212
|
-
ctx.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
step(:apply_mutations) do
|
|
224
|
+
pre = ctx.element.dup # C4.2 — the update set reads the element as it was
|
|
225
|
+
ctx.command.mutations.each do |mutation|
|
|
226
|
+
EntityElement.apply_to_element(@rules, ctx.aggregate, ctx.entity, ctx.element, mutation, ctx.args, pre)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
216
229
|
end
|
|
217
230
|
|
|
218
231
|
def step_advance_lifecycle(ctx)
|
data/lib/hecks/runtime/errors.rb
CHANGED
|
@@ -5,8 +5,34 @@ module Hecks
|
|
|
5
5
|
module Runtime
|
|
6
6
|
class UnknownVerb < StandardError; end
|
|
7
7
|
class EnsuresNotMet < StandardError; end
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
# `detail` — the failing comparison's own resolved operands, "left: X,
|
|
10
|
+
# right: Y" — set only when the given's TOP-LEVEL shape is a bare
|
|
11
|
+
# comparison (`Evaluator.comparison_detail`'s own comment has the full
|
|
12
|
+
# scoping); nil otherwise. Deliberately NOT folded into `#message`:
|
|
13
|
+
# that string is pinned byte-for-byte across this corpus's own specs
|
|
14
|
+
# (`raise_error(GivenNotMet, "...")`, command_rules_spec.rb and every
|
|
15
|
+
# domain that vendors this gem) as CONTRACT, so changing its shape by
|
|
16
|
+
# default would be a breaking change for every one of them. Riding on
|
|
17
|
+
# `#detailed_message` instead (Ruby 3.2+, what irb/a Rails console's
|
|
18
|
+
# own unhandled-exception banner already calls to show more than
|
|
19
|
+
# `#message`) means the detail actually reaches a human staring at a
|
|
20
|
+
# live refusal, without moving the string anything else asserts on.
|
|
21
|
+
class GivenNotMet < StandardError
|
|
22
|
+
attr_reader :detail
|
|
23
|
+
|
|
24
|
+
def initialize(message = nil, detail: nil)
|
|
25
|
+
super(message)
|
|
26
|
+
@detail = detail
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def detailed_message(highlight: false, **opts)
|
|
30
|
+
base = super
|
|
31
|
+
detail ? "#{base} (#{detail})" : base
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class NotFound < StandardError; end
|
|
10
36
|
class LifecycleRefused < StandardError; end
|
|
11
37
|
class TypeMismatch < StandardError; end
|
|
12
38
|
# An argument the command does not declare. Sibling of TypeMismatch : that one
|
|
@@ -3,6 +3,12 @@ require_relative "identity"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Runtime
|
|
6
|
+
# One aggregate (or entity) record's runtime state: the declared
|
|
7
|
+
# attributes hydrated with defaults, plus the identity (`id`) and,
|
|
8
|
+
# for a CAS-capable adapter, the optimistic-concurrency `version`
|
|
9
|
+
# stamped on it. `[]`/`[]=`/method_missing give state-field access;
|
|
10
|
+
# `to_h` is the wire/storage shape with `id` merged in last so a
|
|
11
|
+
# same-named declared attribute can never clobber it.
|
|
6
12
|
class Instance
|
|
7
13
|
attr_reader :aggregate, :id
|
|
8
14
|
attr_accessor :state
|
|
@@ -46,12 +52,12 @@ module Hecks
|
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
def self.defaults(aggregate)
|
|
49
|
-
state = aggregate.attributes.
|
|
55
|
+
state = aggregate.attributes.to_h do |attr|
|
|
50
56
|
# FROZEN, like a list that has had something appended to it.
|
|
51
57
|
# An untouched list is the easiest one to miss and the easiest
|
|
52
58
|
# to mutate: nothing has replaced it yet, so a caller pushing
|
|
53
59
|
# into it writes straight into the aggregate's own state.
|
|
54
|
-
|
|
60
|
+
[attr.name, attr.list? ? Freezer.deep([]) : default_for(aggregate, attr)]
|
|
55
61
|
end
|
|
56
62
|
state[aggregate.lifecycle.field.to_sym] = aggregate.lifecycle.default if aggregate.lifecycle
|
|
57
63
|
state
|
|
@@ -64,7 +70,7 @@ module Hecks
|
|
|
64
70
|
return nil unless aggregate.respond_to?(:value_object)
|
|
65
71
|
|
|
66
72
|
value_object = aggregate.value_object(attribute.type)
|
|
67
|
-
return nil unless value_object
|
|
73
|
+
return nil unless value_object&.attributes&.all? { |field| !field.default.nil? }
|
|
68
74
|
|
|
69
75
|
Value.build(value_object, {}, aggregate)
|
|
70
76
|
end
|
|
@@ -40,8 +40,35 @@ module Hecks
|
|
|
40
40
|
# has to guard ITSELF at the top of its own handler and skip tracing
|
|
41
41
|
# when it does not fire, rather than the caller branching around it —
|
|
42
42
|
# see CommandInterpreter#step_assign_creation_attributes.
|
|
43
|
+
# THE COMMIT BOUNDARY. Every step up to `save` runs as before;
|
|
44
|
+
# `save`, `emit`, and the outbox enqueue that follows them run
|
|
45
|
+
# inside ONE `repository.transaction` — so the aggregate row, its
|
|
46
|
+
# journal entry, the recorded event, and the outbox rows naming
|
|
47
|
+
# who is owed a reaction commit together or not at all. No new
|
|
48
|
+
# step is added to the vocabulary's dispatch order (the step list
|
|
49
|
+
# is a pinned contract, `spec/vocabulary_conformance_spec.rb`);
|
|
50
|
+
# the enqueue is a consequence of `emit`, not a step of its own.
|
|
51
|
+
# A context without a repository (port operations — nothing to
|
|
52
|
+
# save) or with no `:save` in its order runs plainly.
|
|
43
53
|
def run_dispatch_order(order, ctx)
|
|
44
|
-
order.
|
|
54
|
+
split = order.index(:save)
|
|
55
|
+
return order.each { |name| send(:"step_#{name}", ctx) } unless split && ctx.respond_to?(:repository) && ctx.repository
|
|
56
|
+
|
|
57
|
+
order[0...split].each { |name| send(:"step_#{name}", ctx) }
|
|
58
|
+
ctx.repository.transaction do
|
|
59
|
+
order[split..].each { |name| send(:"step_#{name}", ctx) }
|
|
60
|
+
enqueue_outbox(ctx)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Rows for the events this dispatch just emitted, written while
|
|
65
|
+
# the save's transaction is still open. `nil` (no outbox on this
|
|
66
|
+
# repository) tells the dispatcher to react directly, the
|
|
67
|
+
# pre-outbox path; a dry run emits nothing and enqueues nothing.
|
|
68
|
+
def enqueue_outbox(ctx)
|
|
69
|
+
return if ctx.dry_run || !ctx.respond_to?(:outbox_rows=)
|
|
70
|
+
|
|
71
|
+
ctx.outbox_rows = @registry.outbox.enqueue(ctx.repository, Array(ctx.result), ctx.domain)
|
|
45
72
|
end
|
|
46
73
|
|
|
47
74
|
# THE CONCURRENCY-CONTROL SPLIT — see docs/decisions/ (concurrency
|
|
@@ -72,7 +99,7 @@ module Hecks
|
|
|
72
99
|
next unless normalized.key?(attribute.name)
|
|
73
100
|
|
|
74
101
|
Value.refuse_object_reference(command, attribute, normalized[attribute.name])
|
|
75
|
-
normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name])
|
|
102
|
+
normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name], argument: true)
|
|
76
103
|
end
|
|
77
104
|
end
|
|
78
105
|
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -8,6 +8,12 @@ require_relative "registry"
|
|
|
8
8
|
|
|
9
9
|
module Hecks
|
|
10
10
|
module Runtime
|
|
11
|
+
# The boot entry point: loads a bluebook directory (or an explicit
|
|
12
|
+
# file list) into a fresh Registry, runs every registered boot gate
|
|
13
|
+
# (era checks, saga rehydration, …), and hands back the Dispatcher
|
|
14
|
+
# (or RemoteDispatcher, for a domain declaring `dispatched_by
|
|
15
|
+
# ("Lambda")`) bound to it — installing the `Widget::Item.Add(...)`
|
|
16
|
+
# facade sugar unless the caller opts out.
|
|
11
17
|
class Loader
|
|
12
18
|
# `install_facade:` defaults on — every ordinary caller wants
|
|
13
19
|
# `Widget::Item.Add(...)` sugar. A caller that only ever dispatches
|
|
@@ -45,9 +51,24 @@ module Hecks
|
|
|
45
51
|
|
|
46
52
|
run_boot_gates!(registry, directory)
|
|
47
53
|
dispatcher = dispatcher_for(registry)
|
|
54
|
+
redrive_outbox!(dispatcher)
|
|
48
55
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# THE OUTBOX'S BOOT-TIME RECONCILIATION — after the dispatcher
|
|
59
|
+
# exists (a row's consumer runs through its interpreters, so this
|
|
60
|
+
# cannot be a plain registry gate the way saga rehydration is) and
|
|
61
|
+
# after saga rehydration (a redriven row may advance a saga, which
|
|
62
|
+
# must already be in memory). `pending` rows are redriven —
|
|
63
|
+
# provably never started; `claimed` rows are surfaced, never
|
|
64
|
+
# auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
|
|
65
|
+
# no local stores to scan.
|
|
66
|
+
def self.redrive_outbox!(dispatcher)
|
|
67
|
+
return unless dispatcher.respond_to?(:outbox)
|
|
68
|
+
|
|
69
|
+
dispatcher.outbox.redrive!
|
|
70
|
+
end
|
|
71
|
+
|
|
51
72
|
# THE EXPLICIT-FILE FORM — `paths` names the exact bluebook/hecksagon/
|
|
52
73
|
# world files to boot, in place, wherever they actually live. `boot`
|
|
53
74
|
# above only ever takes a directory and globs it; that is the right
|