hecks 1.4.0 → 1.5.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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +29 -5
- data/lib/hecks/projector.rb +74 -6
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +11 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -37,6 +37,20 @@ module Hecks
|
|
|
37
37
|
# its own env var name and passes the resulting string straight
|
|
38
38
|
# through, e.g. `Hecks.boot(path, environment:
|
|
39
39
|
# ENV.fetch("MYAPP_ENV", "development"))`.
|
|
40
|
+
#
|
|
41
|
+
# @param path [String] a domain directory to boot, resolved through
|
|
42
|
+
# `Ports::Loading#bluebook_directory`
|
|
43
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
44
|
+
# resolves it by walking up from the domain directory
|
|
45
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
46
|
+
# facade sugar
|
|
47
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
48
|
+
# domain's own files (e.g. `"production"`); nil loads none
|
|
49
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
50
|
+
# @raise [Errno::ENOENT] if `path` names neither a domain directory nor one holding
|
|
51
|
+
# a `bluebook/` subdirectory
|
|
52
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
53
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
40
54
|
def self.boot(path, shared: nil, install_facade: true, environment: nil)
|
|
41
55
|
loading = Ports::Loading.bootstrap
|
|
42
56
|
directory = loading.bluebook_directory(path)
|
|
@@ -52,6 +66,7 @@ module Hecks
|
|
|
52
66
|
run_boot_gates!(registry, directory)
|
|
53
67
|
dispatcher = dispatcher_for(registry)
|
|
54
68
|
redrive_outbox!(dispatcher)
|
|
69
|
+
seed_privacy_markings!(dispatcher, registry)
|
|
55
70
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
56
71
|
end
|
|
57
72
|
|
|
@@ -63,12 +78,43 @@ module Hecks
|
|
|
63
78
|
# provably never started; `claimed` rows are surfaced, never
|
|
64
79
|
# auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
|
|
65
80
|
# no local stores to scan.
|
|
81
|
+
#
|
|
82
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
83
|
+
# just-booted dispatcher; a no-op unless it exposes an `outbox`
|
|
84
|
+
# @return [void]
|
|
66
85
|
def self.redrive_outbox!(dispatcher)
|
|
67
86
|
return unless dispatcher.respond_to?(:outbox)
|
|
68
87
|
|
|
69
88
|
dispatcher.outbox.redrive!
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
# Turns every `.hecksagon`-declared `AggregateDoor#mark_sensitive` fact into a real
|
|
92
|
+
# `Privacy::Marking.Mark` — after the dispatcher exists (dispatch needs a bound
|
|
93
|
+
# repository), idempotent across reboots (a marking already present is never
|
|
94
|
+
# re-dispatched, the same "provably never started" restraint `redrive_outbox!`
|
|
95
|
+
# holds itself to above). A no-op when nothing declared one, or when the domain
|
|
96
|
+
# never attached Privacy at all.
|
|
97
|
+
#
|
|
98
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the just-booted
|
|
99
|
+
# dispatcher `Privacy::Marking.Mark` dispatches through
|
|
100
|
+
# @param registry [Runtime::Registry] the just-booted registry `pending_privacy_markings`
|
|
101
|
+
# was recorded on
|
|
102
|
+
# @return [void]
|
|
103
|
+
def self.seed_privacy_markings!(dispatcher, registry)
|
|
104
|
+
return if registry.pending_privacy_markings.empty?
|
|
105
|
+
return unless registry.bluebook("Privacy")
|
|
106
|
+
|
|
107
|
+
already_marked_by_domain = registry.pending_privacy_markings.map { |marking| marking[:domain] }.uniq.to_h do |domain|
|
|
108
|
+
[domain, dispatcher.query("Privacy::Marking.ForDomain", domain: domain).map { |row| row[:attribute_path][:value] }]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
registry.pending_privacy_markings.each do |marking|
|
|
112
|
+
next if already_marked_by_domain[marking[:domain]].include?(marking[:attribute_path])
|
|
113
|
+
|
|
114
|
+
dispatcher.dispatch_flat("Privacy::Marking.Mark", marking)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
72
118
|
# The explicit-file form — `paths` names the exact bluebook/hecksagon/
|
|
73
119
|
# world files to boot, in place, wherever they actually live. `boot`
|
|
74
120
|
# above only ever takes a directory and globs it; that is the right
|
|
@@ -91,6 +137,18 @@ module Hecks
|
|
|
91
137
|
# first real path in `paths` — genuinely on disk, not a copy — so
|
|
92
138
|
# every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
|
|
93
139
|
# resolves exactly as an ordinary directory boot's would.
|
|
140
|
+
#
|
|
141
|
+
# @param paths [Array<String>, String] the exact bluebook/hecksagon/world file paths
|
|
142
|
+
# to boot, in the order they should load
|
|
143
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
144
|
+
# resolves it by walking up from `File.dirname` of the first path
|
|
145
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
146
|
+
# facade sugar
|
|
147
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
148
|
+
# named files (e.g. `"production"`); nil loads none
|
|
149
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
150
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
151
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
94
152
|
def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
|
|
95
153
|
loading = Ports::Loading.bootstrap
|
|
96
154
|
files = Array(paths).map { |path| File.expand_path(path) }
|
|
@@ -109,9 +167,11 @@ module Hecks
|
|
|
109
167
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
110
168
|
end
|
|
111
169
|
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
170
|
+
# Runs every registered boot gate against `registry`, in order.
|
|
171
|
+
#
|
|
172
|
+
# ADR 0031 — a per-boot `BootGates` instance holds exactly the gates this
|
|
173
|
+
# registry's own bound adapters have a capability for, rather than two
|
|
174
|
+
# hardcoded, unconditional calls. Ordering is
|
|
115
175
|
# preserved: era-checking (when a persistence plugin contributes one)
|
|
116
176
|
# still runs before `verify!`, saga rehydration still runs after
|
|
117
177
|
# (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
|
|
@@ -124,6 +184,13 @@ module Hecks
|
|
|
124
184
|
# `:post_verify` gates generically; `:saga_rehydration` is the one
|
|
125
185
|
# gate core still registers directly, because ADR 0031 already
|
|
126
186
|
# proved it's not era-specific.
|
|
187
|
+
#
|
|
188
|
+
# @param registry [Runtime::Registry] the registry mid-boot, with its bluebooks,
|
|
189
|
+
# hecksagons, ports, adapters and worlds already loaded
|
|
190
|
+
# @param directory [String] the domain directory the gates report against in a refusal
|
|
191
|
+
# @return [Runtime::BootGates] the gates instance built and run for this boot
|
|
192
|
+
# @raise [Runtime::WiringError] if `registry.verify!` or any contributed gate finds
|
|
193
|
+
# a wiring problem
|
|
127
194
|
def self.run_boot_gates!(registry, directory)
|
|
128
195
|
gates = BootGates.new
|
|
129
196
|
Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
|
|
@@ -149,6 +216,12 @@ module Hecks
|
|
|
149
216
|
# the real, adapter-aware version of this check and refuses by name
|
|
150
217
|
# ("...is bound to Memory") long before this ever would; this only
|
|
151
218
|
# fires when nothing did, because nothing was loaded to.
|
|
219
|
+
#
|
|
220
|
+
# @param registry [Runtime::Registry] the registry mid-boot, whose declared
|
|
221
|
+
# `translations` are checked
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @raise [Runtime::WiringError] if any translation declares a `computes`/`rekeys`
|
|
224
|
+
# rule while no persistence plugin is loaded to interpret it
|
|
152
225
|
def self.check_compute_rules_backstop!(registry)
|
|
153
226
|
return if Ports::Persistence.plugins_loaded?
|
|
154
227
|
|
|
@@ -183,6 +256,12 @@ module Hecks
|
|
|
183
256
|
# this landed. A domain opts in explicitly, the same way
|
|
184
257
|
# `persisted_by("PostgresEra")` is never inferred from anything
|
|
185
258
|
# else either.
|
|
259
|
+
#
|
|
260
|
+
# @param registry [Runtime::Registry] the just-booted registry, whose just-booted
|
|
261
|
+
# domain's `.world` is checked for `dispatched_by("Lambda")`
|
|
262
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] a `RemoteDispatcher`
|
|
263
|
+
# bound to `registry` if the domain declares `dispatched_by("Lambda")`, a plain
|
|
264
|
+
# `Dispatcher` otherwise
|
|
186
265
|
def self.dispatcher_for(registry)
|
|
187
266
|
domain = registry.bluebooks.keys.first
|
|
188
267
|
settings = registry.world(domain)&.for_verb("dispatched_by") || {}
|
|
@@ -191,11 +270,18 @@ module Hecks
|
|
|
191
270
|
RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"), function: settings[:function])
|
|
192
271
|
end
|
|
193
272
|
|
|
194
|
-
#
|
|
195
|
-
# dispatcher
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
# binding lives in the surface a boot
|
|
273
|
+
# Installs the facade sugar (`Widget::Item.Add(...)`), closed over
|
|
274
|
+
# `dispatcher`, and hands `dispatcher` back.
|
|
275
|
+
#
|
|
276
|
+
# The door is installed here, not stamped: the facade's modules close
|
|
277
|
+
# over this dispatcher, so the binding lives in the surface a boot
|
|
278
|
+
# installs rather than in a class-level global (`ruby_class.runtime =`
|
|
279
|
+
# on every aggregate's class) that would make two boots in one process
|
|
280
|
+
# share one name.
|
|
281
|
+
#
|
|
282
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
283
|
+
# just-booted dispatcher the facade should call into
|
|
284
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] `dispatcher`, unchanged
|
|
199
285
|
def self.bind_runtime(dispatcher)
|
|
200
286
|
Facade::Surface.install(dispatcher)
|
|
201
287
|
dispatcher
|
data/lib/hecks/runtime/outbox.rb
CHANGED
|
@@ -13,7 +13,9 @@ module Hecks
|
|
|
13
13
|
# outbound port operation) the external effects those reactions
|
|
14
14
|
# cause. `future-features.md` item 8, built.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
16
|
+
# ## The shape
|
|
17
|
+
#
|
|
18
|
+
# One row per (event, consumer). A consumer is a named
|
|
17
19
|
# policy or process manager that would react to the event — resolved
|
|
18
20
|
# at enqueue time from the registry (`Fanout`), so the outbox records
|
|
19
21
|
# who was owed what, not just that an event happened. Rows move
|
|
@@ -33,18 +35,22 @@ module Hecks
|
|
|
33
35
|
# tracks delivery, not the domain's answer.
|
|
34
36
|
# failed the consumer raised a defect (non-refusal error).
|
|
35
37
|
#
|
|
36
|
-
#
|
|
38
|
+
# ## Delivery is inline by default
|
|
39
|
+
#
|
|
40
|
+
# The dispatcher drains the rows it
|
|
37
41
|
# just wrote, in the same call, in the order C10.2 fixes (per event
|
|
38
42
|
# in `emits` order: that event's policy rows, then its saga rows —
|
|
39
43
|
# the emitting domain's own policies before other domains'). Nothing about the
|
|
40
44
|
# happy path is deferred or asynchronous; a caller still sees every
|
|
41
45
|
# reaction settled when `dispatch` returns. What changes is the
|
|
42
|
-
# crash window: a process that dies between
|
|
43
|
-
#
|
|
44
|
-
# `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
|
|
46
|
+
# crash window: without a durable row, a process that dies between
|
|
47
|
+
# commit and reaction loses the reaction silently. The row survives,
|
|
48
|
+
# and `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
|
|
45
49
|
# finds it.
|
|
46
50
|
#
|
|
47
|
-
#
|
|
51
|
+
# ## What redrive does, and deliberately doesn't
|
|
52
|
+
#
|
|
53
|
+
# A `pending` row is
|
|
48
54
|
# redriven: its consumer provably never started (claiming is the
|
|
49
55
|
# first thing delivery does), so running it now is exactly-once by
|
|
50
56
|
# construction. A `claimed` row is not auto-redriven: the consumer
|
|
@@ -57,7 +63,9 @@ module Hecks
|
|
|
57
63
|
# unique per store, so a re-enqueue of the same fact to the same
|
|
58
64
|
# consumer is a no-op rather than a second row.
|
|
59
65
|
#
|
|
60
|
-
#
|
|
66
|
+
# ## Which adapters
|
|
67
|
+
#
|
|
68
|
+
# Memory (in-process rows — visible to specs,
|
|
61
69
|
# gone with the process, exactly like everything else Memory holds),
|
|
62
70
|
# Sqlite and Postgres (a `hecks_outbox` table in the aggregate's own
|
|
63
71
|
# database — the only way the enqueue can share the save's
|
|
@@ -71,13 +79,30 @@ module Hecks
|
|
|
71
79
|
|
|
72
80
|
Row = Struct.new(:id, :delivery_id, :event_uid, :aggregate, :domain, :kind, :consumer, :event,
|
|
73
81
|
:status, :attempts, :error, keyword_init: true) do
|
|
82
|
+
# Reports whether this row is still waiting to be claimed.
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] true if this row's status is `"pending"`
|
|
74
85
|
def pending? = status == "pending"
|
|
86
|
+
|
|
87
|
+
# Reports whether this row's consumer is currently running.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] true if this row's status is `"claimed"`
|
|
75
90
|
def claimed? = status == "claimed"
|
|
91
|
+
|
|
92
|
+
# Reports whether this row's consumer ran to completion.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean] true if this row's status is `"delivered"`
|
|
76
95
|
def delivered? = status == "delivered"
|
|
96
|
+
|
|
97
|
+
# Reports whether this row's consumer raised a defect.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] true if this row's status is `"failed"`
|
|
77
100
|
def failed? = status == "failed"
|
|
78
101
|
|
|
79
102
|
# Wire-shaped — what an adapter persists. `event` is the event's
|
|
80
103
|
# own `to_h` plus correlation; `Row.event_from` reverses it.
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash{Symbol => Object}] this row's own fields, keyed by name
|
|
81
106
|
def to_h
|
|
82
107
|
{ id: id, delivery_id: delivery_id, event_uid: event_uid, aggregate: aggregate, domain: domain,
|
|
83
108
|
kind: kind, consumer: consumer, event: event, status: status, attempts: attempts, error: error }
|
|
@@ -89,6 +114,12 @@ module Hecks
|
|
|
89
114
|
|
|
90
115
|
module_function
|
|
91
116
|
|
|
117
|
+
# Renders an event as the wire-shaped Hash an outbox row's own `event`
|
|
118
|
+
# field stores.
|
|
119
|
+
#
|
|
120
|
+
# @param event [Runtime::Event] the event to serialize
|
|
121
|
+
# @return [Hash{Symbol => Object}] `event`'s own `to_h`, with `correlation`
|
|
122
|
+
# merged in
|
|
92
123
|
def serialize_event(event)
|
|
93
124
|
event.to_h.merge(correlation: event.correlation)
|
|
94
125
|
end
|
|
@@ -96,11 +127,21 @@ module Hecks
|
|
|
96
127
|
# The emitting domain's own bluebook first, then the rest in load
|
|
97
128
|
# order (C10.2) — the one policy ordering both `PolicyInterpreter#
|
|
98
129
|
# policies_for` and `Fanout.policies` read.
|
|
130
|
+
#
|
|
131
|
+
# @param registry [Runtime::Registry] the booted registry whose loaded
|
|
132
|
+
# bluebooks are ordered
|
|
133
|
+
# @param domain [String, Symbol] the emitting domain, sorted first
|
|
134
|
+
# @return [Array<Bluebook::Chapter>] every loaded chapter, `domain`'s own first
|
|
99
135
|
def bluebooks_home_first(registry, domain)
|
|
100
136
|
home, others = registry.bluebooks.each_value.partition { |bluebook| bluebook.name == domain }
|
|
101
137
|
home + others
|
|
102
138
|
end
|
|
103
139
|
|
|
140
|
+
# Rebuilds a frozen event from an outbox row's own stored `event` field.
|
|
141
|
+
#
|
|
142
|
+
# @param hash [Hash{String, Symbol => Object}] the wire-shaped event Hash,
|
|
143
|
+
# as `serialize_event` built it (String or Symbol keys either way)
|
|
144
|
+
# @return [Runtime::Event] the rebuilt, frozen event
|
|
104
145
|
def event_from(hash)
|
|
105
146
|
hash = hash.transform_keys(&:to_sym)
|
|
106
147
|
Event.new(
|
|
@@ -113,6 +154,12 @@ module Hecks
|
|
|
113
154
|
).emit!
|
|
114
155
|
end
|
|
115
156
|
|
|
157
|
+
# Recursively symbolizes every Hash key reachable from `value`.
|
|
158
|
+
#
|
|
159
|
+
# @param value [Object] the value to symbolize; typically a Hash or Array,
|
|
160
|
+
# possibly nested
|
|
161
|
+
# @return [Object] `value` with every Hash key (at any depth) turned into a
|
|
162
|
+
# Symbol; a non-Hash, non-Array value passes through unchanged
|
|
116
163
|
def deep_symbolize(value)
|
|
117
164
|
case value
|
|
118
165
|
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
@@ -137,6 +184,12 @@ module Hecks
|
|
|
137
184
|
# this consumer".
|
|
138
185
|
# Row order is delivery order (C10.2): per event, in `emits`
|
|
139
186
|
# order — that event's policy rows, then its saga rows.
|
|
187
|
+
#
|
|
188
|
+
# @param registry [Runtime::Registry] the booted registry every candidate
|
|
189
|
+
# consumer is resolved against
|
|
190
|
+
# @param events [Array<Runtime::Event>] the just-emitted events to build rows for
|
|
191
|
+
# @param domain [String, Symbol] the emitting domain
|
|
192
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per (event, consumer)
|
|
140
193
|
def rows_for(registry, events, domain)
|
|
141
194
|
uids = events.to_h { |event| [event, SecureRandom.uuid] }
|
|
142
195
|
events.flat_map do |event|
|
|
@@ -144,6 +197,16 @@ module Hecks
|
|
|
144
197
|
end
|
|
145
198
|
end
|
|
146
199
|
|
|
200
|
+
# Builds one pending row per policy `event` triggers.
|
|
201
|
+
#
|
|
202
|
+
# @param registry [Runtime::Registry] the booted registry policies are
|
|
203
|
+
# resolved against
|
|
204
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
205
|
+
# @param domain [String, Symbol] the emitting domain
|
|
206
|
+
# @param uid [String] this event's own enqueue-time UID, shared by every row
|
|
207
|
+
# built for it
|
|
208
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per matching policy,
|
|
209
|
+
# emitting domain's own bluebook first
|
|
147
210
|
def policies(registry, event, domain, uid)
|
|
148
211
|
emitting = Naming.demodulise(event.aggregate)
|
|
149
212
|
Outbox.bluebooks_home_first(registry, domain).flat_map do |bluebook|
|
|
@@ -159,6 +222,18 @@ module Hecks
|
|
|
159
222
|
end
|
|
160
223
|
end
|
|
161
224
|
|
|
225
|
+
# Builds one pending row per process manager `event` advances, in `domain`
|
|
226
|
+
# only (a saga never reacts across domains).
|
|
227
|
+
#
|
|
228
|
+
# @param registry [Runtime::Registry] the booted registry process managers
|
|
229
|
+
# are resolved against
|
|
230
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
231
|
+
# @param domain [String, Symbol] the domain whose declared process managers
|
|
232
|
+
# are checked
|
|
233
|
+
# @param uid [String] this event's own enqueue-time UID, shared by every row
|
|
234
|
+
# built for it
|
|
235
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per process manager
|
|
236
|
+
# `event` starts, ends, or advances
|
|
162
237
|
def sagas(registry, event, domain, uid)
|
|
163
238
|
bluebook = registry.bluebook(domain)
|
|
164
239
|
return [] unless bluebook
|
|
@@ -170,6 +245,13 @@ module Hecks
|
|
|
170
245
|
end
|
|
171
246
|
end
|
|
172
247
|
|
|
248
|
+
# Reports whether `event` starts, ends, or advances `process_manager`.
|
|
249
|
+
#
|
|
250
|
+
# @param process_manager [Bluebook::ProcessManager] the declared process
|
|
251
|
+
# manager to check
|
|
252
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
253
|
+
# @return [Boolean] true if `event` names `process_manager`'s own
|
|
254
|
+
# `starts_on`, `ends_on`, or a declared handler
|
|
173
255
|
def listens?(process_manager, event)
|
|
174
256
|
process_manager.starts_on == event.name || process_manager.ends_on == event.name ||
|
|
175
257
|
!process_manager.handler_for(event.name).nil?
|
|
@@ -179,6 +261,14 @@ module Hecks
|
|
|
179
261
|
# operation — the row is the durable record that an external
|
|
180
262
|
# call was owed, claimed right before the adapter is asked and
|
|
181
263
|
# settled right after. Everything else is a plain "reaction".
|
|
264
|
+
#
|
|
265
|
+
# @param registry [Runtime::Registry] the booted registry the trigger's own
|
|
266
|
+
# aggregate/port are resolved against
|
|
267
|
+
# @param policy [Bluebook::Policy] the policy whose trigger is classified
|
|
268
|
+
# @param home_domain [String, Symbol] the domain `policy` is declared in,
|
|
269
|
+
# used when `policy` declares no `target_domain`
|
|
270
|
+
# @return [String] `"effect"` when the trigger resolves to an outbound port
|
|
271
|
+
# operation, `"reaction"` otherwise
|
|
182
272
|
def kind_for(registry, policy, home_domain)
|
|
183
273
|
target = "#{policy.target_domain || home_domain}::#{policy.trigger_command}"
|
|
184
274
|
parsed = Naming.split_verb(target)
|
|
@@ -204,11 +294,16 @@ module Hecks
|
|
|
204
294
|
# by the Rust kernel (`spec/rust_conformance_spec.rb`); this is
|
|
205
295
|
# an additive, Ruby-only log, the same rule `saga_dispatch_log`
|
|
206
296
|
# and `policy_dispatch_log` already follow.
|
|
297
|
+
#
|
|
298
|
+
# @param registry [Runtime::Registry] the booted registry this relay drains
|
|
299
|
+
# rows for
|
|
207
300
|
def initialize(registry)
|
|
208
301
|
@registry = registry
|
|
209
302
|
@log = []
|
|
210
303
|
end
|
|
211
304
|
|
|
305
|
+
# Attaches the interpreters a delivered row's own consumer runs through.
|
|
306
|
+
#
|
|
212
307
|
# A Dispatcher hands over the interpreters a consumer runs
|
|
213
308
|
# through (`Dispatcher#initialize`). Until then this relay can
|
|
214
309
|
# enqueue (that needs only the registry) but not deliver — and
|
|
@@ -216,20 +311,38 @@ module Hecks
|
|
|
216
311
|
# to. The registry holds one relay for its lifetime; a second
|
|
217
312
|
# dispatcher fronting the same registry re-attaches, which is
|
|
218
313
|
# fine because both dispatchers share every log and store.
|
|
314
|
+
#
|
|
315
|
+
# @param policies [Runtime::PolicyInterpreter] the interpreter a `"policy:"`
|
|
316
|
+
# consumer reacts through
|
|
317
|
+
# @param sagas [Runtime::SagaInterpreter] the interpreter a `"saga:"` consumer
|
|
318
|
+
# advances through
|
|
319
|
+
# @return [Runtime::Outbox::Relay] self
|
|
219
320
|
def attach(policies:, sagas:)
|
|
220
321
|
@policies = policies
|
|
221
322
|
@sagas = sagas
|
|
222
323
|
self
|
|
223
324
|
end
|
|
224
325
|
|
|
326
|
+
# Reports whether a dispatcher has attached its interpreters.
|
|
327
|
+
#
|
|
328
|
+
# @return [Boolean] true once `attach` has run
|
|
225
329
|
def attached? = !@policies.nil?
|
|
226
330
|
|
|
331
|
+
# Writes one pending row per (event, consumer) `events` owes a reaction to.
|
|
332
|
+
#
|
|
227
333
|
# Called inside the save transaction by `Interpreting` for the
|
|
228
334
|
# command/entity paths, and outside one by `Dispatcher` for port
|
|
229
335
|
# operations (which save nothing, so there is no transaction to
|
|
230
336
|
# share). Returns the rows as stored (ids assigned), or nil when
|
|
231
337
|
# the repository has no outbox — the dispatcher then reacts
|
|
232
338
|
# directly, exactly as before.
|
|
339
|
+
#
|
|
340
|
+
# @param repository [Persistence::AppendOnly] the repository whose store the
|
|
341
|
+
# rows are enqueued into; a no-op unless it has an outbox
|
|
342
|
+
# @param events [Array<Runtime::Event>] the events this dispatch just emitted
|
|
343
|
+
# @param domain [String, Symbol] the emitting domain
|
|
344
|
+
# @return [Array<Runtime::Outbox::Row>, nil] the stored rows, `id` assigned;
|
|
345
|
+
# `nil` when `repository` has no outbox; `[]` when `events` is empty
|
|
233
346
|
def enqueue(repository, events, domain)
|
|
234
347
|
return nil unless repository.outbox?
|
|
235
348
|
return [] if events.empty?
|
|
@@ -241,6 +354,15 @@ module Hecks
|
|
|
241
354
|
|
|
242
355
|
# Drain the rows a dispatch just committed. `rows` nil means "no
|
|
243
356
|
# outbox here" — react directly, the pre-outbox path.
|
|
357
|
+
#
|
|
358
|
+
# @param rows [Array<Runtime::Outbox::Row>, nil] the just-enqueued rows to
|
|
359
|
+
# deliver; nil to react directly instead (no outbox on this repository)
|
|
360
|
+
# @param events [Array<Runtime::Event>] the events this dispatch just emitted,
|
|
361
|
+
# read when `rows` is nil
|
|
362
|
+
# @param domain [String, Symbol] the emitting domain
|
|
363
|
+
# @param repository [Persistence::AppendOnly] the repository `rows` were
|
|
364
|
+
# enqueued into
|
|
365
|
+
# @return [void]
|
|
244
366
|
def deliver(rows, events, domain, repository)
|
|
245
367
|
if rows.nil?
|
|
246
368
|
# Per event, in `emits` order — its policies, then its sagas
|
|
@@ -258,6 +380,11 @@ module Hecks
|
|
|
258
380
|
|
|
259
381
|
# One row: claim, run its consumer, settle. A claim that fails
|
|
260
382
|
# means another relay (or this one, re-entrantly) already has it.
|
|
383
|
+
#
|
|
384
|
+
# @param row [Runtime::Outbox::Row] the row to claim and deliver
|
|
385
|
+
# @param repository [Persistence::AppendOnly] the repository `row` is stored in
|
|
386
|
+
# @return [Boolean] true when this call claimed and delivered `row`; false
|
|
387
|
+
# when the claim failed, or the consumer raised a defect (recorded on `log`)
|
|
261
388
|
def deliver_row(row, repository)
|
|
262
389
|
return false unless repository.outbox_claim(row.id)
|
|
263
390
|
|
|
@@ -283,13 +410,24 @@ module Hecks
|
|
|
283
410
|
end
|
|
284
411
|
|
|
285
412
|
# Every row in every bound store, newest last. `status:` narrows.
|
|
413
|
+
#
|
|
414
|
+
# @param status [String, Symbol, nil] keep only rows with this status; nil
|
|
415
|
+
# for every row
|
|
416
|
+
# @return [Array<Runtime::Outbox::Row>] copies of the matching rows, across
|
|
417
|
+
# every store with an outbox
|
|
286
418
|
def rows(status: nil)
|
|
287
419
|
stores.flat_map { |repository| repository.outbox_rows(status: status) }
|
|
288
420
|
end
|
|
289
421
|
|
|
422
|
+
# Runs the boot-time reconciliation over every bound store.
|
|
423
|
+
#
|
|
290
424
|
# **Boot-time reconciliation**. Redrives `pending` rows (never
|
|
291
425
|
# claimed — safe by construction); surfaces `claimed` rows and
|
|
292
426
|
# redrives them only when told to (`claimed: true`).
|
|
427
|
+
#
|
|
428
|
+
# @param claimed [Boolean] whether to also redrive `claimed` rows (an
|
|
429
|
+
# explicit operator decision); false leaves them surfaced and untouched
|
|
430
|
+
# @return [Array<Runtime::Outbox::Row>] every row this call actually delivered
|
|
293
431
|
def redrive!(claimed: false)
|
|
294
432
|
redriven = []
|
|
295
433
|
stores.each do |repository|
|
|
@@ -16,11 +16,18 @@ module Hecks
|
|
|
16
16
|
class PolicyInterpreter
|
|
17
17
|
attr_reader :registry
|
|
18
18
|
|
|
19
|
+
# @param registry [Runtime::Registry] the booted registry whose loaded
|
|
20
|
+
# bluebooks are scanned for candidate policies
|
|
21
|
+
# @param door [Runtime::Dispatcher] the dispatcher reactions re-enter through,
|
|
22
|
+
# and whose reaction-depth guard is checked before each delivery
|
|
19
23
|
def initialize(registry, door:)
|
|
20
24
|
@registry = registry
|
|
21
25
|
@door = door
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# Fires every declared policy `event` triggers, recording each outcome on
|
|
29
|
+
# the registry's reaction log.
|
|
30
|
+
#
|
|
24
31
|
# `deliver` returns `nil` for a policy whose `where` did not hold —
|
|
25
32
|
# silently, the same as a policy `policies_for` never selected at all
|
|
26
33
|
# (an `event_qualifier` miss carries no reaction_log entry either) —
|
|
@@ -32,6 +39,13 @@ module Hecks
|
|
|
32
39
|
# way of running exactly the consumer a row names (`Runtime::
|
|
33
40
|
# Outbox::Relay#run_consumer`) instead of every policy that
|
|
34
41
|
# matches the event. Selection is otherwise identical.
|
|
42
|
+
#
|
|
43
|
+
# @param event [Runtime::Event] the just-emitted event to react to
|
|
44
|
+
# @param domain [String, Symbol] the domain `event`'s own aggregate belongs
|
|
45
|
+
# to, the emitting domain's policies fire first
|
|
46
|
+
# @param only [Array(Bluebook::Policy, String), nil] one `[policy, home_domain]`
|
|
47
|
+
# pair to run exactly, instead of scanning every loaded bluebook for candidates
|
|
48
|
+
# @return [void]
|
|
35
49
|
def react(event, domain, only: nil)
|
|
36
50
|
selected = only ? [only] : policies_for(event, domain)
|
|
37
51
|
selected.each do |policy, home_domain|
|
|
@@ -195,14 +209,13 @@ module Hecks
|
|
|
195
209
|
# The event's own identity is a fact too, not only its payload. A
|
|
196
210
|
# for_each query commonly filters by the emitting record's own
|
|
197
211
|
# identity (`OpenForCustomer`'s own `reference:`, scoping by the
|
|
198
|
-
# very customer who was just suspended) —
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
# customer" the whole reaction exists to catch.
|
|
212
|
+
# very customer who was just suspended) — a fact routing keeps
|
|
213
|
+
# separate from payload (`to:`/`with:`, what the facade's own
|
|
214
|
+
# bang-methods always use), so `event.payload` alone does not carry
|
|
215
|
+
# it. Without this, the query would silently see neither the field
|
|
216
|
+
# it needs nor any error saying why — an empty result read as
|
|
217
|
+
# "nothing to freeze" instead of "the customer" the whole reaction
|
|
218
|
+
# exists to catch.
|
|
206
219
|
#
|
|
207
220
|
# Merged in only when the query declares an argument by that exact
|
|
208
221
|
# name and the emitting aggregate's own identity is genuinely what
|
|
@@ -261,7 +274,7 @@ module Hecks
|
|
|
261
274
|
label: "#{policy.name}'s trigger"
|
|
262
275
|
)
|
|
263
276
|
|
|
264
|
-
#
|
|
277
|
+
# The raw inputs `args` was resolved from — same additive,
|
|
265
278
|
# Ruby-only shape SagaInterpreter#deliver_saga_dispatch's own
|
|
266
279
|
# saga_dispatch_log gets, for Properties.dispatch_binding_
|
|
267
280
|
# fidelity's own independent re-derivation of Policy#with_spec's
|
|
@@ -25,6 +25,10 @@ module Hecks
|
|
|
25
25
|
refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
|
|
26
26
|
].freeze
|
|
27
27
|
|
|
28
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter dispatches
|
|
29
|
+
# against
|
|
30
|
+
# @param rules [Runtime::CommandRules] the shared command-rule checks (references) this
|
|
31
|
+
# interpreter's steps call
|
|
28
32
|
def initialize(registry, rules:)
|
|
29
33
|
@registry = registry
|
|
30
34
|
@rules = rules
|
|
@@ -32,6 +36,22 @@ module Hecks
|
|
|
32
36
|
|
|
33
37
|
# `invocation` — the `Runtime::Invocation` `Dispatcher` built;
|
|
34
38
|
# `ctx.args` is its `to_args`, `ctx.route` its `target`.
|
|
39
|
+
#
|
|
40
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
41
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the port operation belongs to
|
|
42
|
+
# @param operation [Bluebook::PortOperation] the port operation to dispatch
|
|
43
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for this call
|
|
44
|
+
# @return [Array<Runtime::Event>] the events recorded: the operation's own `emits` for
|
|
45
|
+
# an inbound operation, or a single `answers`/`refuses` event for an outbound one
|
|
46
|
+
# (an adapter failure is recorded as a `refuses` event, not raised)
|
|
47
|
+
# @raise [Runtime::UnknownArgument] if `invocation` offers an argument the operation
|
|
48
|
+
# does not declare
|
|
49
|
+
# @raise [Runtime::AbsentArgument] if `invocation` omits a non-optional declared
|
|
50
|
+
# argument
|
|
51
|
+
# @raise [Runtime::TypeMismatch] if an offered argument does not coerce to its declared
|
|
52
|
+
# type
|
|
53
|
+
# @raise [Runtime::NotFound] if an offered reference or the operation's own receiving
|
|
54
|
+
# record cannot be found
|
|
35
55
|
def call(domain, aggregate, operation, invocation)
|
|
36
56
|
ctx = Context.new(domain, aggregate, operation, invocation.to_args)
|
|
37
57
|
ctx.invocation = invocation
|