Class: Hecks::Runtime::Registry

Inherits:
Object
  • Object
show all
Includes:
SagaPersistence, Verification
Defined in:
lib/hecks/runtime/registry.rb,
lib/hecks/runtime/registry/verification.rb,
lib/hecks/runtime/registry/saga_persistence.rb

Overview

The collections a boot gathers — bluebooks, hexagons, ports, adapters, worlds, the logs — and how a repository is resolved from them. The wiring gate lives in registry/verification.rb, saga persistence resolution in registry/saga_persistence.rb.

Defined Under Namespace

Modules: SagaPersistence, Verification

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SagaPersistence

#rehydrate_sagas!, #saga_persistence

Methods included from Verification

#adapter_class, #check_settings, #check_verb, #port_for, #verify!, #verify_default_adapter!

Constructor Details

#initialize(root: nil) ⇒ Registry

Returns a new instance of Registry.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/hecks/runtime/registry.rb', line 20

def initialize(root: nil)
  @root         = root
  @bluebooks    = {}
  @hecksagons   = {}
  @ports = {}
  @adapters     = {}
  @worlds       = {}
  @translations = []
  @event_log    = []
  @reaction_log = []
  @saga_log = []
  # ADDITIVE, RUBY-ONLY — never merged into saga_log/reaction_log.
  # rust/src/kernel/orchestrate.rs ports THOSE two arrays' exact
  # shape byte-for-byte (spec/rust_conformance_spec.rb's own
  # equality check) — a landmine found by reading that spec before
  # touching anything, not by hitting it. These carry the raw
  # inputs a dispatch's own argument binding was resolved from
  # (SagaInterpreter#deliver_saga_dispatch / PolicyInterpreter#
  # trigger_args), for Properties.dispatch_binding_fidelity's own
  # independent re-derivation — a fact neither existing log
  # records at all, so there is nothing here for Rust to have
  # matched or drifted from.
  @saga_dispatch_log   = []
  @policy_dispatch_log = []
  @saga_instances = Hash.new { |h, k| h[k] = {} }
  # GUARDS `saga_instances`' OWN mutation+checkpoint sequence
  # (`SagaInterpreter`'s 4 write points, §7) — the same shape of
  # hazard this codebase's own prior audit already flagged for
  # `@reaction_depth`, a thread-shared dispatcher ivar with no
  # lock, made meaningfully easier to hit once a persistence write
  # sits in the same critical section. Held across the in-memory
  # mutation AND the checkpoint write together, never across a
  # saga's own dispatch cascade — see `SagaInterpreter#advance_saga`'s
  # own comment for why that distinction matters (non-reentrant
  # Mutex, recursive re-entry is real).
  @saga_mutex = Mutex.new
  @repositories = {}
  @projection_repositories = {}
  @bluebook_builders = {}
end

Instance Attribute Details

#adaptersObject (readonly)

Returns the value of attribute adapters.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def adapters
  @adapters
end

#bluebooksObject (readonly)

Returns the value of attribute bluebooks.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def bluebooks
  @bluebooks
end

#event_logObject (readonly)

Returns the value of attribute event_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def event_log
  @event_log
end

#hecksagonsObject (readonly)

Returns the value of attribute hecksagons.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def hecksagons
  @hecksagons
end

#policy_dispatch_logObject (readonly)

Returns the value of attribute policy_dispatch_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def policy_dispatch_log
  @policy_dispatch_log
end

#portsObject (readonly)

Returns the value of attribute ports.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def ports
  @ports
end

#reaction_logObject (readonly)

Returns the value of attribute reaction_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def reaction_log
  @reaction_log
end

#rootObject (readonly)

Returns the value of attribute root.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def root
  @root
end

#saga_dispatch_logObject (readonly)

Returns the value of attribute saga_dispatch_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_dispatch_log
  @saga_dispatch_log
end

#saga_instancesObject (readonly)

Returns the value of attribute saga_instances.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_instances
  @saga_instances
end

#saga_logObject (readonly)

Returns the value of attribute saga_log.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_log
  @saga_log
end

#saga_mutexObject (readonly)

Returns the value of attribute saga_mutex.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def saga_mutex
  @saga_mutex
end

#translationsObject (readonly)

Returns the value of attribute translations.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def translations
  @translations
end

#worldsObject (readonly)

Returns the value of attribute worlds.



16
17
18
# File 'lib/hecks/runtime/registry.rb', line 16

def worlds
  @worlds
end

Instance Method Details

#add_adapter(item) ⇒ Object



84
# File 'lib/hecks/runtime/registry.rb', line 84

def add_adapter(item) = @adapters[item.name] = item

#add_bluebook(item) ⇒ Object



70
# File 'lib/hecks/runtime/registry.rb', line 70

def add_bluebook(item) = @bluebooks[item.name] = item

#add_hecksagon(item) ⇒ Object

MERGED, NOT REPLACED — RECOVERED, not new (see Runtime::Loader .boot's own comment for the provenance). A domain's hecksagon can now load in more than one block for the same domain (base file plus an environments/<name>.hecksagon overlay), and the second block should ADD to what the first declared, not silently discard it.



78
79
80
81
# File 'lib/hecks/runtime/registry.rb', line 78

def add_hecksagon(item)
  existing = @hecksagons[item.domain]
  @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
end

#add_port(item) ⇒ Object



83
# File 'lib/hecks/runtime/registry.rb', line 83

def add_port(item) = @ports[item.name] = item

#add_translation(item) ⇒ Object



101
# File 'lib/hecks/runtime/registry.rb', line 101

def add_translation(item) = @translations << item

#add_world(item) ⇒ Object

MERGED, NOT REPLACED — the same generalization for World that add_hecksagon above recovers for Hecksagon: an environments/<name>.world overlay (or a host-owned tenancy overlay world, same mechanism) can now add or override settings for a domain a base .world file already declared, without restating everything the base file said. Settings merge shallow, keyed exactly the way WorldBuilder already stores them (both the bare verb key and the "verb:adapter" qualified key point at the same resolved hash) — an overlay's key wins over the base's same key; a key only the base declares survives untouched.



96
97
98
99
# File 'lib/hecks/runtime/registry.rb', line 96

def add_world(item)
  existing = @worlds[item.domain]
  @worlds[item.domain] = existing ? merge_worlds(existing, item) : item
end

#bluebook(name) ⇒ Object



109
# File 'lib/hecks/runtime/registry.rb', line 109

def bluebook(name)  = @bluebooks[name.to_s]

#bluebook_builder(name) ⇒ Object

THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter name — see the comment on BluebookBuilder.build. A chapter split across several files (language/bluebook/*.bluebook, all Hecks.bluebook "Bluebook") needs its declarations to accumulate into ONE builder rather than each file minting its own and silently discarding the one before.



66
67
68
# File 'lib/hecks/runtime/registry.rb', line 66

def bluebook_builder(name)
  @bluebook_builders[name.to_s] ||= yield
end

#capability_graphObject



149
150
151
# File 'lib/hecks/runtime/registry.rb', line 149

def capability_graph
  @capability_graph ||= CapabilityGraph.new(self)
end

#hecksagon(name) ⇒ Object



110
# File 'lib/hecks/runtime/registry.rb', line 110

def hecksagon(name) = @hecksagons[name.to_s]

#merge_hecksagons(a, b) ⇒ Object

RECOVERED — see add_hecksagon's own comment for provenance. Concatenates every list-shaped fact; binds in particular is additive because an overlay REBINDING an aggregate (a new persisted_by for the same aggregate/verb) is meant to shadow the base's own bind at resolution time, not erase it outright — Ports::Persistence::BindingPolicy.resolve's own "exactly one authoritative bind" check is what actually catches a genuine double-bind; this merge only concatenates, it does not itself decide which of two binds for the same aggregate wins.



191
192
193
194
195
196
197
198
199
# File 'lib/hecks/runtime/registry.rb', line 191

def merge_hecksagons(a, b)
  Bluebook::Hecksagon.new(
    domain:             a.domain,
    binds:              a.binds + b.binds,
    subscriptions:      a.subscriptions + b.subscriptions,
    framework_members:  a.framework_members + b.framework_members,
    vendored_bluebooks: a.vendored_bluebooks + b.vendored_bluebooks
  )
end

#merge_worlds(a, b) ⇒ Object

RECOVERED AND GENERALIZED — see add_world's own comment. realm/ latest are scalars, so the overlay's value wins when present, else the base's survives; settings is a shallow merge keyed by verb (and "verb:adapter") — an overlay entry for a key the base also declares REPLACES that key's whole resolved hash (the same all-or-nothing shape WorldBuilder#method_missing already builds each entry as), it does not deep-merge field by field within it.



208
209
210
211
212
213
214
215
# File 'lib/hecks/runtime/registry.rb', line 208

def merge_worlds(a, b)
  Bluebook::World.new(
    domain:   a.domain,
    realm:    b.realm || a.realm,
    latest:   b.latest || a.latest,
    settings: a.settings.merge(b.settings)
  )
end

#projection_current?(projection, authoritative) ⇒ Boolean

Returns:

  • (Boolean)


167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/hecks/runtime/registry.rb', line 167

def projection_current?(projection, authoritative)
  projected_entries = projection.entries
  source_entries = authoritative.entries
  return false unless projected_entries.length == source_entries.length
  return false unless projected_entries.zip(source_entries).all? do |projected, source|
    projected.operation == source.operation && projected.id == source.id && projected.state == source.state
  end

  projected_rows = projection.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
  source_rows = authoritative.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
  projected_rows == source_rows
rescue StandardError
  false
end

#read_repository(domain, aggregate) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/hecks/runtime/registry.rb', line 153

def read_repository(domain, aggregate)
  key = [domain.to_s, aggregate.hecks_name]
  binding = Ports::Projection.binds_for(self, domain, aggregate).first
  return repository(domain, aggregate) unless binding

  projection = (@projection_repositories[key] ||= begin
    projection = Ports::Persistence::RepositoryFactory.build(self, domain, aggregate, binding,
                                                             recover: true, settings_verb: Ports::Projection::VERB)
    projection
  end)
  authoritative = repository(domain, aggregate)
  projection_current?(projection, authoritative) ? projection : authoritative
end

#repository(domain, aggregate) ⇒ Object



115
116
117
# File 'lib/hecks/runtime/registry.rb', line 115

def repository(domain, aggregate)
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
end

#reset_runtime_state!Object

EVERYTHING A DISPATCH WROTE, CLEARED; NOTHING A BOOT DECLARED, TOUCHED. Bluebooks, hecksagons, ports, adapters, worlds and the resolved eras are what loading the files produced and stay as they are; the logs, the saga instances and the repositories are what running commands against them produced, and go back to exactly what a fresh boot of the same files hands out. Dropping the repositories (rather than emptying each) is deliberate: a fresh boot's own repositories are new adapter instances too, so a Memory adapter starts empty and a durable one sees whatever it persisted — the same reading either way. Sagas rehydrate off that store again, the way Loader.boot_files does after verify!.

What this is for: a test runner that used to boot a runtime per test to get isolation (Behaviors::Expectations.run_one) — ~2s a boot, 76 chess behaviours = two and a half minutes of booting the same two files — can now boot once and reset between tests.



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/hecks/runtime/registry.rb', line 136

def reset_runtime_state!
  @event_log.clear
  @reaction_log.clear
  @saga_log.clear
  @saga_dispatch_log.clear
  @policy_dispatch_log.clear
  @saga_instances.clear
  @repositories = {}
  @projection_repositories = {}
  rehydrate_sagas!
  self
end

#resolved_erasObject

name => era ordinal as resolved by the boot-time era gate. A lineage adapter writes into ITS OWN era's partition — which, for an old checkout booting a held-but-superseded shape, is not the newest one.



107
# File 'lib/hecks/runtime/registry.rb', line 107

def resolved_eras = @resolved_eras ||= {}

#verbsObject



113
# File 'lib/hecks/runtime/registry.rb', line 113

def verbs = @bluebooks.values.flat_map(&:verbs).sort

#world(name) ⇒ Object



111
# File 'lib/hecks/runtime/registry.rb', line 111

def world(name)     = @worlds[name.to_s]