hecks 1.3.0 → 1.4.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 +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -13,17 +13,30 @@ module Hecks
|
|
|
13
13
|
# instance really is a fresh store, with nothing shared across tests or
|
|
14
14
|
# tenants.
|
|
15
15
|
class Memory
|
|
16
|
-
#
|
|
16
|
+
# Reports that two tenant boots of this adapter never share state.
|
|
17
|
+
#
|
|
18
|
+
# **Tenant-capable trivially** — see `Runtime::TenantCheck`'s own header
|
|
17
19
|
# for the full reasoning. `@records` is a plain instance variable;
|
|
18
20
|
# two `Runtime.boot` calls build two entirely separate Registry
|
|
19
21
|
# objects and, through them, two entirely separate Memory
|
|
20
22
|
# instances, so two tenant boots never share this adapter's state
|
|
21
23
|
# by construction — nothing here needs to know "tenant" exists.
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] always true
|
|
22
26
|
def self.tenant_capable? = true
|
|
27
|
+
|
|
28
|
+
# Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array<Symbol>] `[:atomic_put]`
|
|
23
31
|
def persistence_capabilities = [:atomic_put]
|
|
24
32
|
|
|
25
33
|
attr_reader :aggregate, :events
|
|
26
34
|
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records this store holds
|
|
36
|
+
# @param settings [Hash] world settings for the binding; accepted for the shared adapter
|
|
37
|
+
# constructor shape and ignored
|
|
38
|
+
# @param root [String, nil] project root directory; accepted for the shared adapter
|
|
39
|
+
# constructor shape and ignored
|
|
27
40
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
28
41
|
@aggregate = aggregate
|
|
29
42
|
@records = {}
|
|
@@ -32,39 +45,90 @@ module Hecks
|
|
|
32
45
|
@outbox = []
|
|
33
46
|
end
|
|
34
47
|
|
|
48
|
+
# Looks up the current record for one aggregate identity.
|
|
49
|
+
#
|
|
50
|
+
# @param id [String, Object] the aggregate identity, compared as `id.to_s`
|
|
51
|
+
# @return [Runtime::Instance, nil] the held record, or nil when no record has that id
|
|
35
52
|
def find(id) = @records[id.to_s]
|
|
53
|
+
|
|
54
|
+
# Counts the records currently held, deleted ones excluded.
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer] number of live records
|
|
36
57
|
def count = @records.size
|
|
37
58
|
|
|
59
|
+
# Lists every held record, in insertion order unless an ordering attribute is given.
|
|
60
|
+
#
|
|
61
|
+
# @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
|
|
62
|
+
# by; nil leaves the records in insertion order
|
|
63
|
+
# @param direction [Symbol, String] `:asc` or `:desc`
|
|
64
|
+
# @return [Array<Runtime::Instance>] the held records, `[]` when the store is empty
|
|
65
|
+
# @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
|
|
38
66
|
def all(order_by: nil, direction: :asc)
|
|
39
67
|
InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
40
68
|
end
|
|
41
69
|
|
|
70
|
+
# Answers a declared query by filtering, ordering and paging the held records in Ruby.
|
|
71
|
+
#
|
|
72
|
+
# @param specification [QuerySpecification::Common::Options] the declared query
|
|
73
|
+
# @param args [Hash{Symbol => Object}] values for the specification's symbolic operands
|
|
74
|
+
# @param context [Hash] execution context; only `:registry` (a `Runtime::Registry` or
|
|
75
|
+
# nil) is read, for comparators that look up another aggregate
|
|
76
|
+
# @return [Array<Runtime::Instance>] the matching records, `[]` when none match
|
|
77
|
+
# @raise [Runtime::WiringError] if a where clause uses an operation no comparator handles
|
|
42
78
|
def query(specification, args = {}, context: {})
|
|
43
79
|
Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
|
|
44
80
|
end
|
|
45
81
|
|
|
82
|
+
# Records one journal entry, holding a codec copy of its state.
|
|
83
|
+
#
|
|
84
|
+
# Through the state codec, like every durable adapter (PR A3): the
|
|
85
|
+
# journal holds `StateCodec.copy` — exactly what an encode-to-JSON
|
|
86
|
+
# then decode would hand back — never the caller's own live state
|
|
87
|
+
# objects, so an entry read back here has the same deep-symbol,
|
|
88
|
+
# plain-Hash shape a Heki/Sqlite/Postgres entry has.
|
|
89
|
+
#
|
|
90
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to journal
|
|
91
|
+
# @return [Ports::Persistence::Entry] the caller's own `entry`, not the journalled copy
|
|
46
92
|
def append(entry)
|
|
47
|
-
|
|
93
|
+
copied = Ports::Persistence::Entry.new(operation: entry.operation, id: entry.id,
|
|
94
|
+
state: copy(entry.state), mirrors: entry.mirrors)
|
|
95
|
+
@entries << copied
|
|
48
96
|
entry
|
|
49
97
|
end
|
|
50
98
|
|
|
99
|
+
# Applies one journal entry to the current-state Hash.
|
|
100
|
+
#
|
|
101
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to materialize
|
|
102
|
+
# @return [Runtime::Instance, nil] the new record for a save; for a delete, the record
|
|
103
|
+
# removed, or nil when none was held
|
|
51
104
|
def project(entry)
|
|
52
105
|
if entry.save?
|
|
53
|
-
@records[entry.id] =
|
|
106
|
+
@records[entry.id] = build_instance(entry)
|
|
54
107
|
else
|
|
55
108
|
@records.delete(entry.id)
|
|
56
109
|
end
|
|
57
110
|
end
|
|
58
111
|
|
|
112
|
+
# Journals and materializes an instance's current state in one call.
|
|
113
|
+
#
|
|
114
|
+
# @param instance [Runtime::Instance] the instance to store
|
|
115
|
+
# @return [Runtime::Instance] the stored record, a fresh instance over a copy of the state
|
|
59
116
|
def save(instance)
|
|
60
|
-
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state
|
|
117
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: copy(instance.state))
|
|
61
118
|
append(entry)
|
|
62
119
|
project(entry)
|
|
63
120
|
end
|
|
64
121
|
|
|
122
|
+
# Stores an entry and reports whether it inserted, replaced or conflicted.
|
|
123
|
+
#
|
|
65
124
|
# One in-memory critical section in the only thread touching this plain
|
|
66
125
|
# Hash: classify and replace without a preliminary repository lookup.
|
|
67
126
|
# Durable append and projection remain ordered exactly as ordinary save.
|
|
127
|
+
#
|
|
128
|
+
# @param entry [Ports::Persistence::Entry] the save to store
|
|
129
|
+
# @param insert_only [Boolean] when true, an existing record is left untouched
|
|
130
|
+
# @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
|
|
131
|
+
# existing record and nothing was written
|
|
68
132
|
def atomic_put(entry, insert_only: false)
|
|
69
133
|
exists = @records.key?(entry.id.to_s)
|
|
70
134
|
return :conflicted if insert_only && exists
|
|
@@ -75,29 +139,44 @@ module Hecks
|
|
|
75
139
|
status
|
|
76
140
|
end
|
|
77
141
|
|
|
142
|
+
# Journals a delete and removes the record, whether or not one is held.
|
|
143
|
+
#
|
|
144
|
+
# @param id [String, Object] the aggregate identity, journalled as `id.to_s`
|
|
145
|
+
# @return [Runtime::Instance, nil] the record removed, or nil when none was held
|
|
78
146
|
def delete(id)
|
|
79
147
|
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
80
148
|
append(entry)
|
|
81
149
|
project(entry)
|
|
82
150
|
end
|
|
83
151
|
|
|
152
|
+
# Appends an emitted event to the in-process event log.
|
|
153
|
+
#
|
|
154
|
+
# @param event [Runtime::Event] the emitted event
|
|
155
|
+
# @return [Array<Runtime::Event>] the live event log, including `event`
|
|
84
156
|
def record_event(event) = @events << event
|
|
85
157
|
|
|
158
|
+
# Lists the journal in append order, for `AppendOnly#recover!` to replay.
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<Ports::Persistence::Entry>] a copy of the journal, `[]` when nothing
|
|
161
|
+
# has been appended
|
|
86
162
|
def entries = @entries.dup
|
|
87
163
|
|
|
164
|
+
# Empties the records, journal, event log and outbox, keeping the adapter itself.
|
|
165
|
+
#
|
|
88
166
|
# Every other driven adapter (Postgres/PostgresEra/Sqlite/D1)
|
|
89
|
-
#
|
|
167
|
+
# implements this too — `Ports::Persistence::AppendOnly#reset!`
|
|
90
168
|
# forwards to it and only raises when the wrapped adapter doesn't
|
|
91
|
-
# respond to `reset!` at all,
|
|
92
|
-
#
|
|
93
|
-
# "cannot be cleared"). Existing callers that fully re-`Hecks.boot`
|
|
169
|
+
# respond to `reset!` at all, and nothing about "in memory" implies
|
|
170
|
+
# "cannot be cleared". Existing callers that fully re-`Hecks.boot`
|
|
94
171
|
# a domain per test case don't need this — they get a brand new
|
|
95
172
|
# `Memory` instance, with brand new empty `@records`/`@events`/
|
|
96
173
|
# `@entries`, for free. This is for the other case: a caller that
|
|
97
|
-
# deliberately keeps
|
|
174
|
+
# deliberately keeps one booted runtime across many cases (to skip
|
|
98
175
|
# `load_domain`'s own per-boot parse/verify cost) and wants each
|
|
99
176
|
# case to start from the same clean slate `Hecks.boot` would have
|
|
100
177
|
# given it, without paying for a fresh boot to get there.
|
|
178
|
+
#
|
|
179
|
+
# @return [Adapters::Memory] self, now empty
|
|
101
180
|
def reset!
|
|
102
181
|
@records = {}
|
|
103
182
|
@events = []
|
|
@@ -106,13 +185,24 @@ module Hecks
|
|
|
106
185
|
self
|
|
107
186
|
end
|
|
108
187
|
|
|
188
|
+
# Runs the block directly, as the save + emit + outbox boundary other adapters commit.
|
|
189
|
+
#
|
|
109
190
|
# No rollback here — a Hash has no transaction to join. Memory
|
|
110
191
|
# implements `transaction` so `Interpreting#run_dispatch_order` has
|
|
111
192
|
# one shape to call, and the outbox so a spec can watch rows move
|
|
112
193
|
# pending → claimed → delivered without a database (the same
|
|
113
194
|
# reason Memory records `events`). See `Runtime::Outbox`.
|
|
195
|
+
#
|
|
196
|
+
# @yield the writes to run together; an exception raised inside undoes nothing
|
|
197
|
+
# @return [Object] the block's own result
|
|
114
198
|
def transaction = yield
|
|
115
199
|
|
|
200
|
+
# Holds new outbox rows, skipping any whose `delivery_id` is already held.
|
|
201
|
+
#
|
|
202
|
+
# @param rows [Array<Runtime::Outbox::Row>] pending rows to enqueue; each accepted row
|
|
203
|
+
# has its `id` assigned in place
|
|
204
|
+
# @return [Array<Runtime::Outbox::Row>] the rows actually enqueued, `[]` when every one
|
|
205
|
+
# was a duplicate
|
|
116
206
|
def outbox_enqueue(rows)
|
|
117
207
|
rows.filter_map do |row|
|
|
118
208
|
next nil if @outbox.any? { |held| held.delivery_id == row.delivery_id }
|
|
@@ -123,6 +213,11 @@ module Hecks
|
|
|
123
213
|
end
|
|
124
214
|
end
|
|
125
215
|
|
|
216
|
+
# Marks a pending outbox row claimed and counts the delivery attempt.
|
|
217
|
+
#
|
|
218
|
+
# @param id [Integer] the row id `outbox_enqueue` assigned
|
|
219
|
+
# @return [Boolean] true when the row was pending and is now claimed; false when it is
|
|
220
|
+
# unknown or no longer pending
|
|
126
221
|
def outbox_claim(id) # rubocop:disable Naming/PredicateMethod
|
|
127
222
|
row = @outbox.find { |held| held.id == id }
|
|
128
223
|
return false unless row&.pending?
|
|
@@ -132,6 +227,13 @@ module Hecks
|
|
|
132
227
|
true
|
|
133
228
|
end
|
|
134
229
|
|
|
230
|
+
# Records a delivery outcome on an outbox row, whatever status it held.
|
|
231
|
+
#
|
|
232
|
+
# @param id [Integer] the row id `outbox_enqueue` assigned
|
|
233
|
+
# @param status [String, Symbol] the new status, one of `Runtime::Outbox::STATUSES`;
|
|
234
|
+
# not validated here
|
|
235
|
+
# @param error [String, nil] the failure description, or nil to clear it
|
|
236
|
+
# @return [Boolean] true when the row exists and was updated; false when no row has `id`
|
|
135
237
|
def outbox_settle(id, status:, error: nil) # rubocop:disable Naming/PredicateMethod
|
|
136
238
|
row = @outbox.find { |held| held.id == id } or return false
|
|
137
239
|
row.status = status.to_s
|
|
@@ -139,10 +241,104 @@ module Hecks
|
|
|
139
241
|
true
|
|
140
242
|
end
|
|
141
243
|
|
|
244
|
+
# Lists outbox rows in enqueue order, as copies a caller may mutate freely.
|
|
245
|
+
#
|
|
246
|
+
# @param status [String, Symbol, nil] only rows with this status; nil lists every row
|
|
247
|
+
# @return [Array<Runtime::Outbox::Row>] shallow copies of the matching rows, `[]` when
|
|
248
|
+
# none match
|
|
142
249
|
def outbox_rows(status: nil)
|
|
143
250
|
rows = status ? @outbox.select { |row| row.status == status.to_s } : @outbox
|
|
144
251
|
rows.map(&:dup)
|
|
145
252
|
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
# THE LANGUAGE'S OWN BOOTSTRAP SAVES ITS SELF-DESCRIPTION QUADRATICALLY
|
|
257
|
+
# OTHERWISE. `MetaValidator::Judge` dispatches every declaration in the
|
|
258
|
+
# self-hosted grammar into a fresh, private, never-durable `Memory`
|
|
259
|
+
# store (meta_validator.rb's own header: "each bluebook is judged in a
|
|
260
|
+
# fresh in-memory store") — and every nested-entity dispatch (a
|
|
261
|
+
# `ValueObject::Member`, then one `ValueObject::Member::Pair` per
|
|
262
|
+
# key/value pair) re-saves the WHOLE parent aggregate, because entities
|
|
263
|
+
# have no storage of their own (S17, ADR 0026). A table of N member
|
|
264
|
+
# rows costs O(N) dispatches, each PAYING TWICE for the aggregate's own
|
|
265
|
+
# size-N state: once in `StateCodec.copy` (`append`/`project`'s own
|
|
266
|
+
# encode-then-decode round trip) and again in `Instance#initialize`'s
|
|
267
|
+
# `hydrate_with_defaults`, which re-walks and re-validates every
|
|
268
|
+
# element of an entity list on EVERY save regardless of how many of
|
|
269
|
+
# them were already valid as of the previous one
|
|
270
|
+
# (`Value::EntityListCoercion#hydrate_entity_list` has no "already
|
|
271
|
+
# hydrated" shortcut for entity elements — only a value-object list
|
|
272
|
+
# element gets one). Both are O(N) per save, so a table of N rows
|
|
273
|
+
# costs O(N^2) total — and it is paid by every rspec worker and every
|
|
274
|
+
# `bin/*` subprocess that boots the language at all (found live: PR
|
|
275
|
+
# #738's 128-row `RefusalSiteArgument` table alone tripled this one
|
|
276
|
+
# aggregate's own save time, 6.7s -> 18.2s).
|
|
277
|
+
#
|
|
278
|
+
# `Runtime::Value.judge_bootstrapping?` (judge.rb's own `send_to`,
|
|
279
|
+
# wrapping every dispatch the judge makes) is already the flag that
|
|
280
|
+
# marks exactly this window and NOTHING else — "never for a REAL
|
|
281
|
+
# domain's own declared value objects... which Judge never dispatches
|
|
282
|
+
# commands against" (coercion.rb's own comment on the same flag). It
|
|
283
|
+
# is reused here rather than a new toggle for the same reason: one
|
|
284
|
+
# flag, one meaning, checked by two unrelated callers for two
|
|
285
|
+
# unrelated purposes (loosening a scalar-shape check there, skipping
|
|
286
|
+
# both round trips here) is simpler to reason about than two flags
|
|
287
|
+
# that would always be true or false together.
|
|
288
|
+
#
|
|
289
|
+
# WHY SKIPPING BOTH IS SAFE HERE, AND ONLY HERE: `entry.state` a save
|
|
290
|
+
# ever hands this adapter is always `instance.state.dup`
|
|
291
|
+
# (`AppendOnly#save`) — a shallow copy of an ALREADY-hydrated,
|
|
292
|
+
# ALREADY-validated live `Instance`'s own state, built by the very
|
|
293
|
+
# same `Value.for_attribute`/`hydrate_with_defaults` machinery
|
|
294
|
+
# `StateCodec.copy` and `Instance.new`'s default (`hydrate: true`)
|
|
295
|
+
# path would otherwise redo. Every VALUE inside it — a `Runtime::
|
|
296
|
+
# Value` (frozen through, see value.rb's own header) or a `list_of`
|
|
297
|
+
# attribute's own array (`Freezer.deep`d the moment it was built,
|
|
298
|
+
# instance.rb's own header on `Instance#dup`) — is already immutable,
|
|
299
|
+
# so a bare top-level `.dup` is exactly as safe as `Instance#dup`
|
|
300
|
+
# already trusts it to be everywhere else in this codebase; nothing
|
|
301
|
+
# below the top level is ever mutated in place. Re-deriving the same
|
|
302
|
+
# answer through the codec and through re-hydration is therefore
|
|
303
|
+
# pure, avoidable cost for this one caller — never a correctness
|
|
304
|
+
# requirement.
|
|
305
|
+
#
|
|
306
|
+
# `bootstrap_fast_path?` is the guard, and it is deliberately CHEAP —
|
|
307
|
+
# O(this aggregate's own declared attribute count), never O(N) — so
|
|
308
|
+
# it cannot reintroduce the very cost it exists to avoid: `StateCodec.
|
|
309
|
+
# decoded?` (the obvious-looking alternative) recurses into every
|
|
310
|
+
# `list_of` element to check IT, which is exactly the O(N) walk this
|
|
311
|
+
# whole change removes. A `Hash` with every top-level key already a
|
|
312
|
+
# `Symbol` is what `Instance#state` ALWAYS looks like (`Value.hydrate`
|
|
313
|
+
# refuses anything else, coercion.rb's own header), so it is checked
|
|
314
|
+
# here instead — true for the actual shape every real save has,
|
|
315
|
+
# false (falling back to the always-correct slow path) for anything
|
|
316
|
+
# that somehow doesn't.
|
|
317
|
+
#
|
|
318
|
+
# Verified empirically before landing, not just argued: instrumenting
|
|
319
|
+
# every bootstrapping-time save across a full `grammar_registry` boot
|
|
320
|
+
# and comparing this fast path's own state, journal entry, and stored
|
|
321
|
+
# `Instance` against the unmodified `StateCodec.copy` + `Instance.new
|
|
322
|
+
# (hydrate: true)` pipeline's found them equal (`==`) for every one of
|
|
323
|
+
# 6,617 saves, zero mismatches.
|
|
324
|
+
def bootstrap_fast_path?(state)
|
|
325
|
+
Runtime::Value.judge_bootstrapping? && state.is_a?(Hash) && state.keys.all?(Symbol)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def copy(state)
|
|
329
|
+
return state.dup if bootstrap_fast_path?(state)
|
|
330
|
+
|
|
331
|
+
Ports::Persistence::StateCodec.copy(@aggregate, state)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def build_instance(entry)
|
|
335
|
+
if bootstrap_fast_path?(entry.state)
|
|
336
|
+
Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup, hydrate: false)
|
|
337
|
+
else
|
|
338
|
+
decoded = Ports::Persistence::StateCodec.copy(@aggregate, entry.state)
|
|
339
|
+
Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: decoded)
|
|
340
|
+
end
|
|
341
|
+
end
|
|
146
342
|
end
|
|
147
343
|
end
|
|
148
344
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Adapters
|
|
5
|
-
# A
|
|
5
|
+
# A generic stand-in for a real Checkout-Session-style adapter — no
|
|
6
6
|
# network call, no real account, matching whatever real payment
|
|
7
7
|
# adapter a project pairs it with by shape alone
|
|
8
8
|
# (`create_session(event:, registration_id:, success_url:, cancel_url:)
|
|
@@ -5,12 +5,12 @@ module Hecks
|
|
|
5
5
|
class Postgres
|
|
6
6
|
# How a state crosses the column boundary: which fields persist, and
|
|
7
7
|
# how each one encodes into (and decodes out of) its column. Same
|
|
8
|
-
#
|
|
8
|
+
# shape as Sqlite::Codec (one column per attribute, JSON for
|
|
9
9
|
# nested/list) — not its mechanics. Sqlite's own columns carry no
|
|
10
10
|
# type affinity worth trusting on the way back out (SQLite gives it
|
|
11
11
|
# back typed anyway via the driver), but `pg` hands every column
|
|
12
|
-
# back as
|
|
13
|
-
# `double precision` column needs its
|
|
12
|
+
# back as text unless a type map says otherwise, so a real `bigint`/
|
|
13
|
+
# `double precision` column needs its own coercion on decode that
|
|
14
14
|
# Sqlite never had to write.
|
|
15
15
|
module Codec
|
|
16
16
|
private
|
|
@@ -23,7 +23,7 @@ module Hecks
|
|
|
23
23
|
fields << { name: lifecycle.field, attribute: nil, sql_type: "text" } if lifecycle && fields.none? do |field|
|
|
24
24
|
field[:name] == lifecycle.field
|
|
25
25
|
end
|
|
26
|
-
# `projects`
|
|
26
|
+
# `projects` fields (S12, ADR 0025) are a local column too — see
|
|
27
27
|
# Sqlite::Codec#persisted_fields' own comment; identical reasoning,
|
|
28
28
|
# `text` to match this file's own lowercase SQL type spelling.
|
|
29
29
|
@aggregate.projected_fields.each do |field|
|
|
@@ -35,12 +35,12 @@ module Hecks
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def encode(attr, value)
|
|
38
|
-
#
|
|
38
|
+
# **Never set is not empty** — same reasoning as Sqlite::Codec's own
|
|
39
39
|
# comment: a list attribute nothing has ever appended to has to
|
|
40
40
|
# stay NULL to answer the same as Memory does, not become `[]`
|
|
41
41
|
# invented by this adapter's own storage.
|
|
42
|
-
return (value.nil? ? nil :
|
|
43
|
-
return
|
|
42
|
+
return (value.nil? ? nil : state_json(value)) if attr.list?
|
|
43
|
+
return state_json(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
|
|
44
44
|
|
|
45
45
|
value
|
|
46
46
|
end
|
|
@@ -51,21 +51,37 @@ module Hecks
|
|
|
51
51
|
encode(field[:attribute], value)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Every jsonb column's text goes through the state codec's `encode`
|
|
55
|
+
# (PR A3) — see Sqlite::Codec#state_json.
|
|
56
|
+
def state_json(value) = JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, value))
|
|
57
|
+
|
|
58
|
+
# Columns reassembled, then decoded once through the state codec
|
|
59
|
+
# (PR A3) — see Sqlite::Codec#decode, including why a NULL
|
|
60
|
+
# projected-only column reads back absent.
|
|
54
61
|
def decode(row)
|
|
55
|
-
persisted_fields.each_with_object({}) do |field,
|
|
62
|
+
state = persisted_fields.each_with_object({}) do |field, raw_state|
|
|
56
63
|
attr = field[:attribute]
|
|
57
64
|
unless attr
|
|
58
|
-
|
|
65
|
+
value = row[field[:name].to_s]
|
|
66
|
+
next if value.nil? && projected_only?(field)
|
|
67
|
+
|
|
68
|
+
raw_state[field[:name]] = value
|
|
59
69
|
next
|
|
60
70
|
end
|
|
61
71
|
raw = row[attr.name.to_s]
|
|
62
|
-
|
|
72
|
+
raw_state[attr.name] =
|
|
63
73
|
if attr.list? || value_object?(attr)
|
|
64
|
-
raw ? JSON.parse(raw
|
|
74
|
+
raw ? JSON.parse(raw) : nil
|
|
65
75
|
else
|
|
66
76
|
coerce_scalar(attr, raw)
|
|
67
77
|
end
|
|
68
78
|
end
|
|
79
|
+
Ports::Persistence::StateCodec.decode(@aggregate, state)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def projected_only?(field)
|
|
83
|
+
@aggregate.lifecycle&.field&.to_sym != field[:name].to_sym &&
|
|
84
|
+
@aggregate.projected_fields.any? { |projected| projected.name.to_sym == field[:name].to_sym }
|
|
69
85
|
end
|
|
70
86
|
|
|
71
87
|
# `pg` returns every column as a Ruby String by default (no type
|
|
@@ -3,7 +3,7 @@ require_relative "../../../runtime/outbox"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
6
|
-
#
|
|
6
|
+
# **The outbox, Postgres-shaped** — shared verbatim by `Postgres` and the
|
|
7
7
|
# era plugin's `PostgresEra`, the same way their `events` and
|
|
8
8
|
# `hecks_saga_instances` DDL is copied between them: nothing here is
|
|
9
9
|
# lineage-specific. Needs `@db` (a `PG::Connection`) and `table`
|
|
@@ -12,10 +12,19 @@ module Hecks
|
|
|
12
12
|
# for the SQL idioms (`ON CONFLICT DO NOTHING` = idempotent enqueue,
|
|
13
13
|
# `WHERE status = 'pending'` = the compare-and-set claim).
|
|
14
14
|
module PostgresOutbox
|
|
15
|
-
#
|
|
15
|
+
# Runs the block inside one Postgres transaction, joining an already-open one.
|
|
16
|
+
#
|
|
17
|
+
# **Re-entrant** — `Interpreting#run_dispatch_order` opens one
|
|
16
18
|
# transaction around save+emit+outbox and the adapter's own
|
|
17
19
|
# `append`/`atomic_put`/`delete` each open theirs; PG refuses
|
|
18
20
|
# BEGIN inside BEGIN, so an inner call joins the open one.
|
|
21
|
+
#
|
|
22
|
+
# @yield the writes to commit together; an exception raised inside rolls the
|
|
23
|
+
# outermost transaction back
|
|
24
|
+
# @return [Object] the block's own result
|
|
25
|
+
# @raise [PG::ConnectionBad] if the connection died; a reconnect is attempted for the
|
|
26
|
+
# next caller before it is re-raised
|
|
27
|
+
# @raise [PG::Error] if a statement inside the block fails
|
|
19
28
|
def transaction(&)
|
|
20
29
|
return yield unless @db.transaction_status == PG::PQTRANS_IDLE
|
|
21
30
|
|
|
@@ -25,6 +34,13 @@ module Hecks
|
|
|
25
34
|
raise
|
|
26
35
|
end
|
|
27
36
|
|
|
37
|
+
# Inserts new outbox rows as pending, skipping any whose `delivery_id` already exists.
|
|
38
|
+
#
|
|
39
|
+
# @param rows [Array<Runtime::Outbox::Row>] rows to enqueue; each accepted row has its
|
|
40
|
+
# `id` and `status` assigned in place. `row.aggregate` is stored as given
|
|
41
|
+
# @return [Array<Runtime::Outbox::Row>] the rows actually inserted, `[]` when every one
|
|
42
|
+
# was a duplicate
|
|
43
|
+
# @raise [PG::Error] if an insert fails
|
|
28
44
|
def outbox_enqueue(rows)
|
|
29
45
|
rows.filter_map do |row|
|
|
30
46
|
result = pg_exec_params(
|
|
@@ -40,6 +56,12 @@ module Hecks
|
|
|
40
56
|
end
|
|
41
57
|
end
|
|
42
58
|
|
|
59
|
+
# Claims a pending outbox row with a compare-and-set update, counting the attempt.
|
|
60
|
+
#
|
|
61
|
+
# @param id [Integer] the row id `outbox_enqueue` assigned
|
|
62
|
+
# @return [Boolean] true when the row was pending and is now claimed; false when it is
|
|
63
|
+
# unknown or another claimer got there first
|
|
64
|
+
# @raise [PG::Error] if the update fails
|
|
43
65
|
def outbox_claim(id) # rubocop:disable Naming/PredicateMethod
|
|
44
66
|
pg_exec_params(
|
|
45
67
|
"UPDATE hecks_outbox SET status = 'claimed', attempts = attempts + 1, claimed_at = now() " \
|
|
@@ -48,6 +70,15 @@ module Hecks
|
|
|
48
70
|
).cmd_tuples == 1
|
|
49
71
|
end
|
|
50
72
|
|
|
73
|
+
# Records a delivery outcome and its settle time on an outbox row, whatever status it
|
|
74
|
+
# held.
|
|
75
|
+
#
|
|
76
|
+
# @param id [Integer] the row id `outbox_enqueue` assigned
|
|
77
|
+
# @param status [String, Symbol] the new status, one of `Runtime::Outbox::STATUSES`;
|
|
78
|
+
# not validated here
|
|
79
|
+
# @param error [String, nil] the failure description, or nil to store NULL
|
|
80
|
+
# @return [Boolean] true when exactly one row was updated; false when no row has `id`
|
|
81
|
+
# @raise [PG::Error] if the update fails
|
|
51
82
|
def outbox_settle(id, status:, error: nil) # rubocop:disable Naming/PredicateMethod
|
|
52
83
|
pg_exec_params(
|
|
53
84
|
"UPDATE hecks_outbox SET status = $2, error = $3, settled_at = now() WHERE id = $1",
|
|
@@ -55,6 +86,13 @@ module Hecks
|
|
|
55
86
|
).cmd_tuples == 1
|
|
56
87
|
end
|
|
57
88
|
|
|
89
|
+
# Lists the outbox rows whose `aggregate` column equals this adapter's `table`, in
|
|
90
|
+
# enqueue order.
|
|
91
|
+
#
|
|
92
|
+
# @param status [String, Symbol, nil] only rows with this status; nil lists every row
|
|
93
|
+
# @return [Array<Runtime::Outbox::Row>] the matching rows, `event` parsed with Symbol
|
|
94
|
+
# keys; `[]` when none match
|
|
95
|
+
# @raise [PG::Error] if the statement fails
|
|
58
96
|
def outbox_rows(status: nil)
|
|
59
97
|
sql = "SELECT * FROM hecks_outbox WHERE aggregate = $1"
|
|
60
98
|
binds = [table]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Adapters
|
|
3
|
-
#
|
|
3
|
+
# **Self-healing connection** — shared verbatim by `Postgres` and the era
|
|
4
4
|
# plugin's `PostgresEra`, the same way `PostgresOutbox` (outbox.rb) is:
|
|
5
5
|
# nothing here is lineage-specific, and `PostgresOutbox`'s own
|
|
6
6
|
# `@db.exec*` calls route through this module's `pg_exec`/
|
|
@@ -12,18 +12,25 @@ module Hecks
|
|
|
12
12
|
# A backend killed out from under an adapter (a DBA's own
|
|
13
13
|
# `pg_terminate_backend`, a load balancer's failover, a restart) —
|
|
14
14
|
# chaos-tested against the plain `Postgres` adapter: `PG::ConnectionBad`
|
|
15
|
-
# on the query that hit it, and
|
|
15
|
+
# on the query that hit it, and permanently on every query after,
|
|
16
16
|
# since nothing ever replaced `@db` with a live connection.
|
|
17
17
|
# `pg_exec`/`pg_exec_params` are the two primitives every other method
|
|
18
18
|
# in either class funnels through — wrapping them here, once,
|
|
19
|
-
# self-heals `@db` for the
|
|
20
|
-
#
|
|
19
|
+
# self-heals `@db` for the next caller. The current call still
|
|
20
|
+
# raises — reconnecting cannot tell a caller whether its own write
|
|
21
21
|
# reached the server before the connection died, so silently
|
|
22
22
|
# retrying it here could silently double it; that ambiguity is
|
|
23
23
|
# exactly why `Runtime::SagaInterpreter`'s own defect-retry exists
|
|
24
|
-
#
|
|
24
|
+
# one layer up, where a dispatch is retried as a whole (fresh
|
|
25
25
|
# hydrate, fresh `given`s), not as a lone SQL statement.
|
|
26
26
|
module PostgresReconnect
|
|
27
|
+
# Runs one parameterless statement, replacing a dead connection before re-raising.
|
|
28
|
+
#
|
|
29
|
+
# @param sql [String] the statement to run
|
|
30
|
+
# @return [PG::Result] the statement's result
|
|
31
|
+
# @raise [PG::ConnectionBad] if the connection died; `@db` is reconnected for the next
|
|
32
|
+
# caller, and this call is never retried
|
|
33
|
+
# @raise [PG::Error] if the server rejects the statement
|
|
27
34
|
def pg_exec(sql)
|
|
28
35
|
@db.exec(sql)
|
|
29
36
|
rescue PG::ConnectionBad
|
|
@@ -31,6 +38,15 @@ module Hecks
|
|
|
31
38
|
raise
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
# Runs one statement with bind parameters, replacing a dead connection before
|
|
42
|
+
# re-raising.
|
|
43
|
+
#
|
|
44
|
+
# @param sql [String] the statement, with `$1`-style placeholders
|
|
45
|
+
# @param binds [Array<Object>] one value per placeholder, in order; nil binds NULL
|
|
46
|
+
# @return [PG::Result] the statement's result
|
|
47
|
+
# @raise [PG::ConnectionBad] if the connection died; `@db` is reconnected for the next
|
|
48
|
+
# caller, and this call is never retried
|
|
49
|
+
# @raise [PG::Error] if the server rejects the statement
|
|
34
50
|
def pg_exec_params(sql, binds)
|
|
35
51
|
@db.exec_params(sql, binds)
|
|
36
52
|
rescue PG::ConnectionBad
|
|
@@ -40,11 +56,11 @@ module Hecks
|
|
|
40
56
|
|
|
41
57
|
private
|
|
42
58
|
|
|
43
|
-
#
|
|
59
|
+
# **Best-effort** — a reconnect attempt that itself fails (the server
|
|
44
60
|
# is actually down, not just this one backend) leaves `@db`
|
|
45
61
|
# unchanged; the `PG::ConnectionBad` already being re-raised by
|
|
46
62
|
# `pg_exec`/`pg_exec_params` above still reaches the caller either
|
|
47
|
-
# way, so swallowing a failed
|
|
63
|
+
# way, so swallowing a failed reconnect attempt here loses no
|
|
48
64
|
# information — it only avoids masking the original error with a
|
|
49
65
|
# second one.
|
|
50
66
|
def reconnect!
|