hecks 1.4.0 → 1.5.1
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.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -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/compliance.bluebook +221 -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 +85 -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/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- 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/projections.rb +3 -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 +47 -10
- data/lib/hecks/projector.rb +101 -19
- 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 +14 -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/framework/bluebook/compliance.bluebook +0 -1
|
@@ -25,6 +25,12 @@ module Hecks
|
|
|
25
25
|
|
|
26
26
|
attr_reader :aggregate, :path, :events
|
|
27
27
|
|
|
28
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate this store persists
|
|
29
|
+
# @param settings [Hash] adapter settings; `dir:`/`"dir"` (a storage directory, `"data"`
|
|
30
|
+
# by default) and `domain:`/`"domain"` (the saga-persistence scope, defaulting to
|
|
31
|
+
# `aggregate.name`) are read
|
|
32
|
+
# @param root [String, nil] the directory `settings[:dir]` resolves relative to when it
|
|
33
|
+
# is not absolute; defaults to the process's current working directory
|
|
28
34
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
29
35
|
@aggregate = aggregate
|
|
30
36
|
@path = resolve_path(settings, root)
|
|
@@ -47,6 +53,10 @@ module Hecks
|
|
|
47
53
|
FileUtils.mkdir_p(File.dirname(@path))
|
|
48
54
|
end
|
|
49
55
|
|
|
56
|
+
# Reads one record's current projected state.
|
|
57
|
+
#
|
|
58
|
+
# @param id [String, Object] the record's identity, compared as `id.to_s`
|
|
59
|
+
# @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
|
|
50
60
|
def find(id)
|
|
51
61
|
record = store[id.to_s]
|
|
52
62
|
return nil unless record
|
|
@@ -54,13 +64,23 @@ module Hecks
|
|
|
54
64
|
instance(id.to_s, record)
|
|
55
65
|
end
|
|
56
66
|
|
|
67
|
+
# Lists every record currently projected, sorted by id then reordered as requested.
|
|
68
|
+
#
|
|
69
|
+
# @param order_by [String, Symbol, nil] an attribute name to sort by; nil keeps id order
|
|
70
|
+
# @param direction [Symbol] `:asc` or `:desc`
|
|
71
|
+
# @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
|
|
57
72
|
def all(order_by: nil, direction: :asc)
|
|
58
73
|
records = store.sort_by { |id, _| id }.map { |id, record| instance(id, record) }
|
|
59
74
|
InMemoryOrdering.ordered(records, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
60
75
|
end
|
|
61
76
|
|
|
77
|
+
# Counts the records currently projected.
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer] number of stored records, not of journal entries
|
|
62
80
|
def count = store.size
|
|
63
81
|
|
|
82
|
+
# Answers a declared query specification against the projected records.
|
|
83
|
+
#
|
|
64
84
|
# `registry: context[:registry]` — Memory's own `query` already
|
|
65
85
|
# threads this through; Heki's own never did, which made
|
|
66
86
|
# `none_in_state?` (Ports::Query::InMemory) unconditionally
|
|
@@ -68,10 +88,21 @@ module Hecks
|
|
|
68
88
|
# the target up" default) for every `none_in_state` where-clause
|
|
69
89
|
# against a Heki-backed aggregate — silently excluding nothing,
|
|
70
90
|
# always, no matter the actual target state.
|
|
91
|
+
#
|
|
92
|
+
# @param specification [QuerySpecification::Common::Options,
|
|
93
|
+
# Bluebook::Behaviour::ReadModel::FilteredOptions] the declared query specification
|
|
94
|
+
# @param args [Hash{Symbol => Object}] bound values for the specification's placeholders
|
|
95
|
+
# @param context [Hash{Symbol => Object}] call context; `:registry` is read and passed
|
|
96
|
+
# through for registry-aware comparisons
|
|
97
|
+
# @return [Array<Runtime::Instance>] the matching records, ordered and paged
|
|
71
98
|
def query(specification, args = {}, context: {})
|
|
72
99
|
Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
|
|
73
100
|
end
|
|
74
101
|
|
|
102
|
+
# Writes one entry to the durable journal, before any projection of it.
|
|
103
|
+
#
|
|
104
|
+
# @param entry [Persistence::Entry] the save or delete to journal
|
|
105
|
+
# @return [Persistence::Entry] `entry`, unchanged
|
|
75
106
|
def append(entry)
|
|
76
107
|
@entry_mirrors = entry.mirrors
|
|
77
108
|
append_entry(entry.operation, entry.id, entry.state)
|
|
@@ -80,22 +111,42 @@ module Hecks
|
|
|
80
111
|
@entry_mirrors = nil
|
|
81
112
|
end
|
|
82
113
|
|
|
114
|
+
# Applies one journaled entry to the current-state snapshot.
|
|
115
|
+
#
|
|
83
116
|
# Reads fresh rather than trusting the memoized `store` — under
|
|
84
117
|
# `with_lock`, another process may have projected a snapshot since
|
|
85
118
|
# this one last read it, and mutating *its* stale copy would
|
|
86
119
|
# overwrite that write on disk rather than layer on top of it.
|
|
120
|
+
#
|
|
121
|
+
# On a delete, though, the record `current.delete` finds is already
|
|
122
|
+
# gone: every caller (`#delete` above, `AppendOnly#delete`) appends
|
|
123
|
+
# before it projects, so the fresh read above has already replayed
|
|
124
|
+
# this very entry off the journal. What the memoized `store` last
|
|
125
|
+
# held — from before this call — is read up front, for the return
|
|
126
|
+
# value only; it plays no part in what gets written.
|
|
127
|
+
#
|
|
128
|
+
# @param entry [Persistence::Entry] the save or delete to materialize
|
|
129
|
+
# @return [Runtime::Instance, nil] on a save, the newly stored record; on a delete, the
|
|
130
|
+
# removed record, or nil when no record had that id
|
|
87
131
|
def project(entry)
|
|
132
|
+
removed = entry.delete? ? store[entry.id] : nil
|
|
88
133
|
current = read
|
|
89
134
|
if entry.save?
|
|
90
135
|
current[entry.id] = Ports::Persistence::StateCodec.encode(@aggregate, entry.state)
|
|
136
|
+
projected = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
91
137
|
else
|
|
92
138
|
current.delete(entry.id)
|
|
139
|
+
projected = removed && instance(entry.id, removed)
|
|
93
140
|
end
|
|
94
141
|
write(current)
|
|
95
142
|
@store = current
|
|
96
|
-
|
|
143
|
+
projected
|
|
97
144
|
end
|
|
98
145
|
|
|
146
|
+
# Journals and projects an instance's state under the file lock.
|
|
147
|
+
#
|
|
148
|
+
# @param instance [Runtime::Instance] the record to persist
|
|
149
|
+
# @return [Runtime::Instance] `instance`, unchanged
|
|
99
150
|
def save(instance)
|
|
100
151
|
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
101
152
|
with_lock do
|
|
@@ -105,6 +156,11 @@ module Hecks
|
|
|
105
156
|
instance
|
|
106
157
|
end
|
|
107
158
|
|
|
159
|
+
# Journals and projects the removal of one record, if it exists, under the file lock.
|
|
160
|
+
#
|
|
161
|
+
# @param id [String, Object] the record's identity, compared as `id.to_s`
|
|
162
|
+
# @return [Boolean] true when a record was found and deleted, false when there was none
|
|
163
|
+
# and nothing was journaled
|
|
108
164
|
def delete(id)
|
|
109
165
|
return false unless find(id)
|
|
110
166
|
|
|
@@ -116,20 +172,55 @@ module Hecks
|
|
|
116
172
|
true
|
|
117
173
|
end
|
|
118
174
|
|
|
175
|
+
# Records one emitted event in this adapter's in-memory event log.
|
|
176
|
+
#
|
|
177
|
+
# @param event [Runtime::Event] the event to record
|
|
178
|
+
# @return [Array<Runtime::Event>] the adapter's in-memory event log, including `event`
|
|
119
179
|
def record_event(event) = @events << event
|
|
120
180
|
|
|
121
181
|
# ── the optional saga-persistence capability (§2) — Heki's own
|
|
122
182
|
# shape (a sibling snapshot+journal file pair, `SagaStore`,
|
|
123
183
|
# heki/saga_store.rb) rather than a table in a store this adapter
|
|
124
184
|
# doesn't have.
|
|
185
|
+
#
|
|
186
|
+
# @param process_manager [String, Symbol] the process manager's name, compared as
|
|
187
|
+
# `.to_s`
|
|
188
|
+
# @param correlation [String, Symbol, Object] the instance's correlation value, compared
|
|
189
|
+
# as `.to_s`
|
|
190
|
+
# @param state [String, Symbol] the saga's current state name, compared as `.to_s`
|
|
191
|
+
# @param memory [Hash] the saga's working memory to persist
|
|
192
|
+
# @param completed_compensations [Array] the ledger of completed compensable legs;
|
|
193
|
+
# `[]` when none
|
|
194
|
+
# @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the write;
|
|
195
|
+
# callers ignore it
|
|
125
196
|
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
126
197
|
saga_store.save_saga(@domain, process_manager.to_s, correlation.to_s, state.to_s, memory, completed_compensations)
|
|
127
198
|
end
|
|
128
199
|
|
|
200
|
+
# Removes a finished saga instance's checkpoint; a missing one is not an error.
|
|
201
|
+
#
|
|
202
|
+
# @param process_manager [String, Symbol] the process manager's name, compared as
|
|
203
|
+
# `.to_s`
|
|
204
|
+
# @param correlation [String, Symbol, Object] the instance's correlation value, compared
|
|
205
|
+
# as `.to_s`
|
|
206
|
+
# @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the delete;
|
|
207
|
+
# callers ignore it
|
|
129
208
|
def delete_saga(process_manager:, correlation:)
|
|
130
209
|
saga_store.delete_saga(@domain, process_manager.to_s, correlation.to_s)
|
|
131
210
|
end
|
|
132
211
|
|
|
212
|
+
# Yields every checkpointed saga instance of this domain, for `Registry
|
|
213
|
+
# #rehydrate_sagas!` to restore at boot.
|
|
214
|
+
#
|
|
215
|
+
# @yieldparam process_manager [String] the process manager's name
|
|
216
|
+
# @yieldparam correlation [String] the instance's correlation value
|
|
217
|
+
# @yieldparam state [String] the saga's state name
|
|
218
|
+
# @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
|
|
219
|
+
# depth
|
|
220
|
+
# @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
|
|
221
|
+
# when none was recorded
|
|
222
|
+
# @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
|
|
223
|
+
# when no block is given; otherwise `SagaStore`'s internal records Hash
|
|
133
224
|
def each_saga(&) = saga_store.each_saga(@domain, &)
|
|
134
225
|
|
|
135
226
|
private
|
|
@@ -159,7 +250,7 @@ module Hecks
|
|
|
159
250
|
|
|
160
251
|
# `dir: :default` — a bare Symbol, the framework's own convention
|
|
161
252
|
# for "a declared value that resolves by convention, never a silent
|
|
162
|
-
# fallback" —
|
|
253
|
+
# fallback" — would otherwise crash `File.join` outright
|
|
163
254
|
# (`TypeError: no implicit conversion of Symbol into String`):
|
|
164
255
|
# `resolve_path` only ever checked for a missing `dir` setting,
|
|
165
256
|
# never a Symbol one. Treated the same as no setting at all — falls
|
|
@@ -8,11 +8,21 @@ module Hecks
|
|
|
8
8
|
module IdentityRegistry
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
+
# Looks up the id of the identity an authenticated (issuer, subject) pair is linked to,
|
|
12
|
+
# querying the Identity framework bluebook's own `ResolvedBy`.
|
|
13
|
+
#
|
|
11
14
|
# `nil` for a pair nothing has linked, the first match otherwise —
|
|
12
15
|
# `ResolvedBy` is a lookup by the exact (issuer, subject) an
|
|
13
16
|
# authenticated token carries, not a listing, so more than one row
|
|
14
17
|
# would mean two links share a pair, which `Link`'s own natural
|
|
15
18
|
# key already prevents by construction.
|
|
19
|
+
#
|
|
20
|
+
# @param registry [Runtime::Registry] the booted registry, queried for the linked
|
|
21
|
+
# identity's id
|
|
22
|
+
# @param issuer [String] the OIDC issuer that authenticated the caller, compared as a
|
|
23
|
+
# String
|
|
24
|
+
# @param subject [String] the OIDC subject the issuer vouches for, compared as a String
|
|
25
|
+
# @return [String, nil] the linked identity's id, or nil if nothing has linked this pair
|
|
16
26
|
def resolve(registry, issuer:, subject:)
|
|
17
27
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
18
28
|
"Identity::ExternalIdentifier.ResolvedBy",
|
|
@@ -16,10 +16,24 @@ module Hecks
|
|
|
16
16
|
module InMemoryOrdering
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
|
+
# Orders decoded records by a declared attribute, then by id as the total-order
|
|
20
|
+
# tiebreaker.
|
|
21
|
+
#
|
|
19
22
|
# order_by is a runtime value (an HTTP query param, in the
|
|
20
23
|
# console's case), not framework-authored bluebook source — see
|
|
21
24
|
# postgres.rb's own all for the full reasoning. Whitelisted the
|
|
22
25
|
# identical way before FieldPath.dig ever runs.
|
|
26
|
+
#
|
|
27
|
+
# @param records [Array<Runtime::Instance>] the records to order
|
|
28
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `records` belong to, checked for
|
|
29
|
+
# `order_by`'s attribute
|
|
30
|
+
# @param order_by [String, Symbol, nil] a dotted attribute path to sort by; nil returns
|
|
31
|
+
# `records` unchanged
|
|
32
|
+
# @param direction [Symbol] `:asc` or `:desc`
|
|
33
|
+
# @return [Array<Runtime::Instance>] `records`, ordered by `order_by` then id; unchanged
|
|
34
|
+
# when `order_by` is nil
|
|
35
|
+
# @raise [Runtime::WiringError] if `order_by` names no attribute of `aggregate` and is
|
|
36
|
+
# not its lifecycle field
|
|
23
37
|
def ordered(records, aggregate:, order_by:, direction:)
|
|
24
38
|
return records unless order_by
|
|
25
39
|
|
|
@@ -36,6 +50,14 @@ module Hecks
|
|
|
36
50
|
end
|
|
37
51
|
end
|
|
38
52
|
|
|
53
|
+
# Resolves the dotted path `FieldPath.dig` should read to compare a value object field.
|
|
54
|
+
#
|
|
55
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `field` belongs to
|
|
56
|
+
# @param field [String, Symbol] a dotted order_by path, such as `"price"` or
|
|
57
|
+
# `"price.cents"`
|
|
58
|
+
# @return [String] `field` unchanged for an already-dotted path, the lifecycle field, or
|
|
59
|
+
# an attribute with no value-object type; otherwise `"<name>.<member>"` naming the
|
|
60
|
+
# attribute's numeric member, its sole member, or the bare `"value"` convention
|
|
39
61
|
def sortable_path(aggregate, field)
|
|
40
62
|
name, *path = field.to_s.split(".")
|
|
41
63
|
return field.to_s unless path.empty?
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
# The real `key_vault` fulfillment for a single running process — a
|
|
6
|
+
# table of key material, keyed by an opaque reference never exposed to
|
|
7
|
+
# a bluebook or an event. Cryptoshredding only works if the key
|
|
8
|
+
# genuinely leaves memory when destroyed: `destroy` deletes the Hash
|
|
9
|
+
# entry outright, not merely a flag, so a `fetch` afterward has
|
|
10
|
+
# nothing left to decrypt with.
|
|
11
|
+
#
|
|
12
|
+
# **Not crash-durable** — a process restart loses every key this
|
|
13
|
+
# adapter ever issued, along with every ciphertext under it. A
|
|
14
|
+
# deployment that needs destruction to survive a restart (or to leave
|
|
15
|
+
# a physical-destruction audit trail) backs this port with a real
|
|
16
|
+
# `KMS` or `HSM` instead; this default asks for nothing external to
|
|
17
|
+
# run, the same tradeoff `SecureRandomIdentity` already makes for
|
|
18
|
+
# identity minting.
|
|
19
|
+
module InProcessKeyVault
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Mints and stores a fresh symmetric key, returning only an opaque handle to it.
|
|
23
|
+
#
|
|
24
|
+
# @param subject_id [String] the data subject the key is being issued for; held
|
|
25
|
+
# alongside the key material for {#destroy}'s own bookkeeping, never returned
|
|
26
|
+
# @return [String] an opaque key reference; the key material itself never leaves this
|
|
27
|
+
# adapter
|
|
28
|
+
def issue(subject_id:)
|
|
29
|
+
key_reference = SecureRandom.uuid
|
|
30
|
+
(@keys ||= {})[key_reference] = { subject_id: subject_id, secret: SecureRandom.hex(32) }
|
|
31
|
+
key_reference
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Looks up the live key material behind a reference, for encrypting or decrypting.
|
|
35
|
+
#
|
|
36
|
+
# @param key_reference [String] the opaque reference {#issue} returned
|
|
37
|
+
# @return [String, nil] the key's hex-encoded secret, or nil once {#destroy} has run
|
|
38
|
+
def fetch(key_reference) = (@keys ||= {})[key_reference]&.fetch(:secret)
|
|
39
|
+
|
|
40
|
+
# Irrevocably deletes a key's material, so {#fetch} can never answer for it again.
|
|
41
|
+
#
|
|
42
|
+
# @param key_reference [String] the opaque reference {#issue} returned
|
|
43
|
+
# @return [Boolean] true when a key was held and is now gone; false when this
|
|
44
|
+
# reference was already destroyed, or never issued
|
|
45
|
+
def destroy(key_reference:) = !(@keys ||= {}).delete(key_reference).nil? # rubocop:disable Naming/PredicateMethod
|
|
46
|
+
|
|
47
|
+
# Resets the vault to empty, forgetting every key this process ever issued.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def reset! = @keys = {}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -9,6 +9,8 @@ module Hecks
|
|
|
9
9
|
# domain-specific: neither caller needs to know an AWS SDK is
|
|
10
10
|
# involved at all.
|
|
11
11
|
#
|
|
12
|
+
# ## Resolving the function name
|
|
13
|
+
#
|
|
12
14
|
# Function name is computed unless it is named — `"hecks-#{domain}"`,
|
|
13
15
|
# lowercased, matches bin/project_deploy's own `stack_name`
|
|
14
16
|
# exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
|
|
@@ -19,6 +21,8 @@ module Hecks
|
|
|
19
21
|
# reproduces the same string bin/project_deploy computes from the
|
|
20
22
|
# directory.
|
|
21
23
|
#
|
|
24
|
+
# ## When the assumption breaks
|
|
25
|
+
#
|
|
22
26
|
# That assumption is not always true, and when it breaks nothing
|
|
23
27
|
# about it is recoverable from here. `bin/project_deploy` honours
|
|
24
28
|
# a `.world`'s own `stack_prefix`/`stack_name` — settings that
|
|
@@ -33,6 +37,8 @@ module Hecks
|
|
|
33
37
|
# that does not exist — found live, and visible in the deployed
|
|
34
38
|
# journal having never received a single row.
|
|
35
39
|
#
|
|
40
|
+
# ## Naming it explicitly
|
|
41
|
+
#
|
|
36
42
|
# So the name can be named, in the one place the rest of this
|
|
37
43
|
# deployment is already described: the `.world`'s own
|
|
38
44
|
# `persisted_by("Lambda")`/`dispatched_by("Lambda")` block, beside
|
|
@@ -40,6 +46,12 @@ module Hecks
|
|
|
40
46
|
# is unchanged, which is every domain whose stack name was never
|
|
41
47
|
# pinned.
|
|
42
48
|
class Client
|
|
49
|
+
# @param domain [String, Symbol] the bluebook's own declared domain name; computes
|
|
50
|
+
# the function name when `function` is not given
|
|
51
|
+
# @param region [String] the AWS region to invoke in
|
|
52
|
+
# @param function [String, Symbol, nil] an explicit Lambda function name, from a
|
|
53
|
+
# `.world`'s `persisted_by("Lambda")`/`dispatched_by("Lambda")` block; nil computes
|
|
54
|
+
# `"hecks-#{domain.downcase}"`
|
|
43
55
|
def initialize(domain:, region:, function: nil)
|
|
44
56
|
require "aws-sdk-lambda"
|
|
45
57
|
@function_name = function.to_s.empty? ? "hecks-#{domain.to_s.downcase}" : function.to_s
|
|
@@ -48,21 +60,37 @@ module Hecks
|
|
|
48
60
|
|
|
49
61
|
# The function this client actually invokes — read by
|
|
50
62
|
# `Runtime::WiringError` messages and worth asserting on
|
|
51
|
-
# directly, since "which function did we call" is
|
|
52
|
-
#
|
|
63
|
+
# directly, since "which function did we call" is otherwise
|
|
64
|
+
# unanswerable from outside.
|
|
53
65
|
attr_reader :function_name
|
|
54
66
|
|
|
67
|
+
# Reads the whole domain's current state and event log from the remote function.
|
|
68
|
+
#
|
|
55
69
|
# **The whole domain, every time** — matches dispatch::read's own
|
|
56
70
|
# rehydrate-the-full-journal design (Phase 1, rust/host). No
|
|
57
|
-
# caching here, deliberately
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
71
|
+
# caching here, deliberately: memoizing this across calls once
|
|
72
|
+
# silently served stale reads for a warm web process's whole
|
|
73
|
+
# lifetime after a write happened elsewhere — see Lambda#instances's
|
|
74
|
+
# own comment on the real, live bug that caught it.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash{String => Object}] the parsed JSON response; the deployed function's
|
|
77
|
+
# own top-level keys (`"instances"`, `"events"`, …)
|
|
78
|
+
# @raise [Runtime::WiringError] if the function reports a `functionError` (see
|
|
79
|
+
# `invoke`)
|
|
62
80
|
def read
|
|
63
81
|
invoke({ "read" => true })
|
|
64
82
|
end
|
|
65
83
|
|
|
84
|
+
# Dispatches one command to the remote function and returns its parsed response.
|
|
85
|
+
#
|
|
86
|
+
# @param verb [String] the fully-qualified verb to dispatch
|
|
87
|
+
# @param args [Hash] the command's declared arguments
|
|
88
|
+
# @param role [String, Symbol, nil] the role to dispatch as; omitted from the payload
|
|
89
|
+
# when nil
|
|
90
|
+
# @return [Hash{String => Object}] the parsed JSON response, including a `"refusals"`
|
|
91
|
+
# array and a `"mutations"` array
|
|
92
|
+
# @raise [Runtime::WiringError] if the function reports a `functionError` (see
|
|
93
|
+
# `invoke`)
|
|
66
94
|
def dispatch(verb, args, role: nil)
|
|
67
95
|
payload = { "verb" => verb, "args" => args }
|
|
68
96
|
payload["role"] = role if role
|
|
@@ -29,6 +29,16 @@ module Hecks
|
|
|
29
29
|
|
|
30
30
|
attr_reader :aggregate
|
|
31
31
|
|
|
32
|
+
# Resolves which Lambda function's Store to read from and builds the client that
|
|
33
|
+
# reads it.
|
|
34
|
+
#
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose records this adapter reads
|
|
36
|
+
# @param settings [Hash{Symbol, String => Object}] world settings for the binding:
|
|
37
|
+
# `domain` (prefixes the instances lookup; default the aggregate's own name),
|
|
38
|
+
# `region` (default `"us-east-1"`) and `function` (the function name, when it is not
|
|
39
|
+
# `hecks-<domain>`), each read under a Symbol or a String key
|
|
40
|
+
# @param root [String, nil] the boot's own project directory, used with `DOMAIN_NAME`
|
|
41
|
+
# to resolve which function to call; nil falls further back to `domain`
|
|
32
42
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
33
43
|
@aggregate = aggregate
|
|
34
44
|
domain =
|
|
@@ -80,16 +90,40 @@ module Hecks
|
|
|
80
90
|
@prefix = "#{domain}::#{aggregate.hecks_name}#"
|
|
81
91
|
end
|
|
82
92
|
|
|
93
|
+
# Looks up the current record for one aggregate identity, reading the Lambda's own
|
|
94
|
+
# Store fresh on every call.
|
|
95
|
+
#
|
|
96
|
+
# @param id [String, Object] the aggregate identity, compared as `id.to_s`
|
|
97
|
+
# @return [Runtime::Instance, nil] the decoded record, or nil when no record has that id
|
|
83
98
|
def find(id)
|
|
84
99
|
instances[id.to_s]
|
|
85
100
|
end
|
|
86
101
|
|
|
102
|
+
# Lists every stored record, in id order unless an ordering attribute is given.
|
|
103
|
+
#
|
|
104
|
+
# @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
|
|
105
|
+
# by; nil orders by id alone
|
|
106
|
+
# @param direction [Symbol, String] `:asc` or `:desc`
|
|
107
|
+
# @return [Array<Runtime::Instance>] the decoded records, `[]` when the Store holds none
|
|
108
|
+
# with this aggregate's own prefix
|
|
109
|
+
# @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
|
|
87
110
|
def all(order_by: nil, direction: :asc)
|
|
88
111
|
InMemoryOrdering.ordered(instances.values, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
89
112
|
end
|
|
90
113
|
|
|
114
|
+
# Counts the records currently held for this aggregate.
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer] number of records
|
|
91
117
|
def count = instances.size
|
|
92
118
|
|
|
119
|
+
# Answers a declared query by filtering, ordering and paging the held records in Ruby.
|
|
120
|
+
#
|
|
121
|
+
# @param specification [QuerySpecification::Common::Options] the declared query
|
|
122
|
+
# @param args [Hash{Symbol => Object}] values for the specification's symbolic operands
|
|
123
|
+
# @param context [Hash] execution context from `Ports::Query.execute`; accepted for the
|
|
124
|
+
# port's call shape and not read
|
|
125
|
+
# @return [Array<Runtime::Instance>] the matching records, `[]` when none match
|
|
126
|
+
# @raise [Runtime::WiringError] if a where clause uses an operation no comparator handles
|
|
93
127
|
def query(specification, args = {}, context: {})
|
|
94
128
|
Ports::Query::InMemory.execute(instances.values, specification, args)
|
|
95
129
|
end
|
|
@@ -103,10 +137,17 @@ module Hecks
|
|
|
103
137
|
# 1 — there is no local write-ahead log for `recover!` to replay),
|
|
104
138
|
# `append`/`project` raise rather than silently no-op.
|
|
105
139
|
|
|
140
|
+
# Reads a world setting under either a Symbol or a String key, with a fallback.
|
|
141
|
+
#
|
|
106
142
|
# A `.world` block's settings arrive symbol-keyed from the DSL and
|
|
107
143
|
# string-keyed from a round-tripped export, so every read has to
|
|
108
144
|
# accept both — one helper rather than the same five lines per
|
|
109
145
|
# key.
|
|
146
|
+
#
|
|
147
|
+
# @param settings [Hash] the world settings Hash to read from
|
|
148
|
+
# @param key [Symbol] the setting name, tried as itself and as `key.to_s`
|
|
149
|
+
# @param fallback [Object] the value to return when neither key is present
|
|
150
|
+
# @return [Object] the setting's value, or `fallback` when absent
|
|
110
151
|
def setting(settings, key, fallback)
|
|
111
152
|
return settings[key] if settings.key?(key)
|
|
112
153
|
return settings[key.to_s] if settings.key?(key.to_s)
|
|
@@ -121,8 +162,8 @@ module Hecks
|
|
|
121
162
|
# held by the registry `RUNTIME = Hecks.boot(...)` builds once
|
|
122
163
|
# per Lambda web process — WebFunction's own top-level constant,
|
|
123
164
|
# reused warm across every HTTP request that process serves, not
|
|
124
|
-
# rebuilt per request the way
|
|
125
|
-
#
|
|
165
|
+
# rebuilt per request the way memoizing here would wrongly assume).
|
|
166
|
+
# A real, live bug caught this: a mutation
|
|
126
167
|
# dispatched fine (RemoteDispatcher always calls the dispatch
|
|
127
168
|
# Lambda fresh) and the very next `.all` on the same warm
|
|
128
169
|
# container kept returning the state from before that mutation,
|
|
@@ -35,11 +35,20 @@ module Hecks
|
|
|
35
35
|
# browser tab is exactly one origin, exactly one user; there is no
|
|
36
36
|
# second tenant this in-process Hash could ever confuse a first
|
|
37
37
|
# one with.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean] true, always
|
|
38
40
|
def self.tenant_capable? = true
|
|
41
|
+
|
|
42
|
+
# Lists the optional persistence behaviours this adapter advertises.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<Symbol>] `[:atomic_put]`
|
|
39
45
|
def persistence_capabilities = [:atomic_put]
|
|
40
46
|
|
|
41
47
|
attr_reader :aggregate, :events
|
|
42
48
|
|
|
49
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate this store persists
|
|
50
|
+
# @param settings [Hash] accepted but not read by this class
|
|
51
|
+
# @param root [String, nil] accepted but not read by this class
|
|
43
52
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
44
53
|
@aggregate = aggregate
|
|
45
54
|
@records = {}
|
|
@@ -47,22 +56,48 @@ module Hecks
|
|
|
47
56
|
@entries = []
|
|
48
57
|
end
|
|
49
58
|
|
|
59
|
+
# Reads one record's current projected state.
|
|
60
|
+
#
|
|
61
|
+
# @param id [String, Object] the record's identity, compared as `id.to_s`
|
|
62
|
+
# @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
|
|
50
63
|
def find(id) = @records[id.to_s]
|
|
64
|
+
|
|
65
|
+
# Counts the records currently projected.
|
|
66
|
+
#
|
|
67
|
+
# @return [Integer] number of stored records
|
|
51
68
|
def count = @records.size
|
|
52
69
|
|
|
70
|
+
# Lists every record currently projected.
|
|
71
|
+
#
|
|
72
|
+
# @param order_by [String, Symbol, nil] an attribute name to sort by; nil keeps
|
|
73
|
+
# insertion order
|
|
74
|
+
# @param direction [Symbol] `:asc` or `:desc`
|
|
75
|
+
# @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
|
|
53
76
|
def all(order_by: nil, direction: :asc)
|
|
54
77
|
InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
|
|
55
78
|
end
|
|
56
79
|
|
|
80
|
+
# Answers a declared query specification against the projected records.
|
|
81
|
+
#
|
|
57
82
|
# The decision the guide asks for, made explicitly: no compiled
|
|
58
83
|
# dialect of its own, same as Heki/Memory — a personal-scale local
|
|
59
84
|
# store answering by walking `all` is correct on day one, and
|
|
60
85
|
# nothing about a browser tab's own data volume asks for pushdown.
|
|
86
|
+
#
|
|
87
|
+
# @param specification [QuerySpecification::Common::Options,
|
|
88
|
+
# Bluebook::Behaviour::ReadModel::FilteredOptions] the declared query specification
|
|
89
|
+
# @param args [Hash{Symbol => Object}] bound values for the specification's placeholders
|
|
90
|
+
# @param context [Hash{Symbol => Object}] call context; `:registry` is read and passed
|
|
91
|
+
# through for registry-aware comparisons
|
|
92
|
+
# @return [Array<Runtime::Instance>] the matching records, ordered and paged
|
|
61
93
|
def query(specification, args = {}, context: {})
|
|
62
94
|
Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
|
|
63
95
|
end
|
|
64
96
|
|
|
65
|
-
#
|
|
97
|
+
# Records one journal entry, holding a codec copy of its state, the same as `Memory#append`.
|
|
98
|
+
#
|
|
99
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to journal
|
|
100
|
+
# @return [Ports::Persistence::Entry] the caller's own `entry`, not the journalled copy
|
|
66
101
|
def append(entry)
|
|
67
102
|
copied = Ports::Persistence::Entry.new(operation: entry.operation, id: entry.id,
|
|
68
103
|
state: copy(entry.state), mirrors: entry.mirrors)
|
|
@@ -70,6 +105,11 @@ module Hecks
|
|
|
70
105
|
entry
|
|
71
106
|
end
|
|
72
107
|
|
|
108
|
+
# Applies one journaled entry to the current-state store.
|
|
109
|
+
#
|
|
110
|
+
# @param entry [Persistence::Entry] the save or delete to materialize
|
|
111
|
+
# @return [Runtime::Instance, nil] on a save, the newly stored record; on a delete, the
|
|
112
|
+
# removed record, or nil when no record had that id
|
|
73
113
|
def project(entry)
|
|
74
114
|
if entry.save?
|
|
75
115
|
@records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: copy(entry.state))
|
|
@@ -78,12 +118,24 @@ module Hecks
|
|
|
78
118
|
end
|
|
79
119
|
end
|
|
80
120
|
|
|
121
|
+
# Journals and projects an instance's state.
|
|
122
|
+
#
|
|
123
|
+
# @param instance [Runtime::Instance] the record to persist
|
|
124
|
+
# @return [Runtime::Instance] the newly stored record (a copy of `instance`'s state)
|
|
81
125
|
def save(instance)
|
|
82
126
|
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: copy(instance.state))
|
|
83
127
|
append(entry)
|
|
84
128
|
project(entry)
|
|
85
129
|
end
|
|
86
130
|
|
|
131
|
+
# Writes an entry through an existence check, refusing to replace an existing record
|
|
132
|
+
# when asked.
|
|
133
|
+
#
|
|
134
|
+
# @param entry [Persistence::Entry] the entry to persist (its `id` checked for an
|
|
135
|
+
# existing record)
|
|
136
|
+
# @param insert_only [Boolean] true to refuse replacing a record that already exists
|
|
137
|
+
# @return [Symbol] `:conflicted` when `insert_only` met an existing record and nothing
|
|
138
|
+
# was written; `:inserted` or `:replaced` otherwise
|
|
87
139
|
def atomic_put(entry, insert_only: false)
|
|
88
140
|
exists = @records.key?(entry.id.to_s)
|
|
89
141
|
return :conflicted if insert_only && exists
|
|
@@ -94,16 +146,30 @@ module Hecks
|
|
|
94
146
|
status
|
|
95
147
|
end
|
|
96
148
|
|
|
149
|
+
# Journals and projects the removal of one record.
|
|
150
|
+
#
|
|
151
|
+
# @param id [String, Object] the record's identity, compared as `id.to_s`
|
|
152
|
+
# @return [Runtime::Instance, nil] the removed record, or nil when no record had that id
|
|
97
153
|
def delete(id)
|
|
98
154
|
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
99
155
|
append(entry)
|
|
100
156
|
project(entry)
|
|
101
157
|
end
|
|
102
158
|
|
|
159
|
+
# Records one emitted event in this adapter's in-memory event log.
|
|
160
|
+
#
|
|
161
|
+
# @param event [Runtime::Event] the event to record
|
|
162
|
+
# @return [Array<Runtime::Event>] the adapter's in-memory event log, including `event`
|
|
103
163
|
def record_event(event) = @events << event
|
|
104
164
|
|
|
165
|
+
# Reads the whole journal, oldest entry first.
|
|
166
|
+
#
|
|
167
|
+
# @return [Array<Persistence::Entry>] a shallow copy of every appended entry
|
|
105
168
|
def entries = @entries.dup
|
|
106
169
|
|
|
170
|
+
# Clears the stored records, events and journal so a kept runtime starts clean.
|
|
171
|
+
#
|
|
172
|
+
# @return [LocalStorage] self, so a caller can chain after resetting
|
|
107
173
|
def reset!
|
|
108
174
|
@records = {}
|
|
109
175
|
@events = []
|