Class: Hecks::Runtime::Loader
- Inherits:
-
Object
- Object
- Hecks::Runtime::Loader
- Defined in:
- lib/hecks/runtime/loader.rb
Class Method Summary collapse
-
.bind_runtime(dispatcher) ⇒ Object
THE DOOR IS INSTALLED HERE, NOT STAMPED.
-
.boot(path, shared: nil, install_facade: true, environment: nil) ⇒ Object
install_facade:defaults on — every ordinary caller wantsWidget::Item.Add(...)sugar. -
.boot_files(paths, shared: nil, install_facade: true, environment: nil) ⇒ Object
THE EXPLICIT-FILE FORM —
pathsnames the exact bluebook/hecksagon/ world files to boot, in place, wherever they actually live. -
.check_compute_rules_backstop!(registry) ⇒ Object
The one piece of the old, era-owned
check_compute_rules!core still carries — deliberately thinner. -
.dispatcher_for(registry) ⇒ Object
RemoteDispatcherfor a domain routed through Lambda,Dispatcherotherwise — the ONE place this decision gets made, so everything built on top (Handle,AggregateDoor,Facade::Surface) never has to know which class it's holding. -
.run_boot_gates!(registry, directory) ⇒ Object
ADR 0031 — replaces two previously-hardcoded, unconditional calls with a per-boot
BootGatesinstance holding exactly the gates THIS registry's own bound adapters have a capability for.
Class Method Details
.bind_runtime(dispatcher) ⇒ Object
THE DOOR IS INSTALLED HERE, NOT STAMPED. This used to write the
dispatcher onto every aggregate's class (ruby_class.runtime =) — the
class-level global that made two boots in one process share one
name. The facade's modules close over THIS dispatcher instead, so the
binding lives in the surface a boot installs, not on anything shared.
178 179 180 181 |
# File 'lib/hecks/runtime/loader.rb', line 178 def self.bind_runtime(dispatcher) Facade::Surface.install(dispatcher) dispatcher end |
.boot(path, shared: nil, install_facade: true, environment: nil) ⇒ Object
install_facade: defaults on — every ordinary caller wants
Widget::Item.Add(...) sugar. A caller that only ever dispatches
by FQN string (SmokeTest, the one caller so far) can pass
false to skip it: Facade::Surface.install puts a bare global
Ruby constant on Object per domain AND per aggregate name, with
no scoping and no cleanup hook, so a tool booting arbitrary
throwaway domains under generic names ("Widget", "Item", "Tag")
would otherwise leak those names into the rest of the process —
measured, not hypothetical: this exact leak once made an
unrelated dsl_spec.rb example resolve a bare Widget constant
to a stale smoke-test facade from a deleted temp directory instead
of raising, corrupting that spec's own unrelated build. Skipping
the install is safe because nothing downstream of a raw
Dispatcher needs the sugar — Dispatcher#dispatch/#query work
identically either way.
environment: — see Adapters::Folder#load_domain's own comment
for the mechanism. hecks never reads ENV itself (every other
env-var lookup in this codebase lives in app-owned .world/
.hecksagon files, never library internals) — a caller resolves
its own env var name and passes the resulting string straight
through, e.g. Hecks.boot(path, environment: ENV.fetch("MYAPP_ENV", "development")).
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/hecks/runtime/loader.rb', line 34 def self.boot(path, shared: nil, install_facade: true, environment: nil) loading = Ports::Loading.bootstrap directory = loading.bluebook_directory(path) root = loading.shared_root(shared, directory) registry = Registry.new(root: File.dirname(directory)) Hecks.with_registry(registry) do loading.load_library loading.load_project(root) loading.load_domain(directory, environment: environment) end run_boot_gates!(registry, directory) dispatcher = dispatcher_for(registry) install_facade ? bind_runtime(dispatcher) : dispatcher end |
.boot_files(paths, shared: nil, install_facade: true, environment: nil) ⇒ Object
THE EXPLICIT-FILE FORM — paths names the exact bluebook/hecksagon/
world files to boot, in place, wherever they actually live. boot
above only ever takes a directory and globs it; that is the right
shape for a real deployment (examples/banking, a domain someone
cds into), and the wrong one for a caller that wants to declare a
narrow, explicit scope and have it booted exactly as declared — a
.behaviors file's own loads line is the motivating caller
(Hecks::Behaviors), but this carries no behaviors-specific
logic and is not gated behind requiring that module.
NO COPYING, NO TEMP DIRECTORY. A prior port of this same idea
(vendored into a downstream consumer, read before writing this)
scoped a per-test boot by copying files into Dir.mktmpdir — which
destroys real relative paths, and worse, makes a persisted_by
path resolve against the TEMP copy's root instead of the project's
own (confirmed there: a file adapter kept reading and writing the
same deterministic tmp copy across an entire session, because
Hecks.boot's own root is always File.dirname of whatever
directory it was handed). directory here is File.dirname of the
FIRST real path in paths — genuinely on disk, not a copy — so
every downstream path (EraCheck, persisted_by, shared_root)
resolves exactly as an ordinary directory boot's would.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/hecks/runtime/loader.rb', line 73 def self.boot_files(paths, shared: nil, install_facade: true, environment: nil) loading = Ports::Loading.bootstrap files = Array(paths).map { |path| File.(path) } directory = File.dirname(files.first) root = loading.shared_root(shared, directory) registry = Registry.new(root: File.dirname(directory)) Hecks.with_registry(registry) do loading.load_library loading.load_project(root) loading.load_selected(files, environment: environment) end run_boot_gates!(registry, directory) dispatcher = dispatcher_for(registry) install_facade ? bind_runtime(dispatcher) : dispatcher end |
.check_compute_rules_backstop!(registry) ⇒ Object
The one piece of the old, era-owned check_compute_rules! core
still carries — deliberately thinner. registry.translations is
plain Bluebook::Translation/TranslationAggregate/
TranslationCompute/TranslationRekey data (bluebook/ translation.rb, core, no era-specific class involved), so this
needs nothing plugin-specific to ask "does anything declare a
compute/rekey rule at all." A LOADED persistence plugin (e.g. the
era plugin's own :era_compute_rules gate, registered above) runs
the real, adapter-aware version of this check and refuses by name
("...is bound to Memory") long before this ever would; this only
fires when nothing did, because nothing was loaded to.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/hecks/runtime/loader.rb', line 131 def self.check_compute_rules_backstop!(registry) return if Ports::Persistence.plugins_loaded? registry.translations.each do |translation| translation.aggregates.each do |aggregate| next if aggregate.computes.empty? && aggregate.rekeys.empty? raise WiringError, "cannot boot #{translation.domain}::#{aggregate.name}: a compute/rekey rule is declared, but no " \ "persistence plugin that can interpret it is loaded (e.g. require " \ "\"hecks/ports/persistence/plugins/era\")" end end end |
.dispatcher_for(registry) ⇒ Object
RemoteDispatcher for a domain routed through Lambda,
Dispatcher otherwise — the ONE place this decision gets
made, so everything built on top (Handle, AggregateDoor,
Facade::Surface) never has to know which class it's holding.
registry.bluebooks.keys.first is the just-booted domain's own
name (insertion order — the target's own bluebook loads before
any uses_framework chapter, bin/project_rust's own header
draws the identical distinction), not a directory basename.
dispatched_by("Lambda") is its OWN, EXPLICIT verb — NOT
inferred from deployed_to("AwsLambda")'s mere presence. Both
Banking's and Embryonaut's .world files already declare
deployed_to("AwsLambda") (it only means "a deploy target
exists"), so treating that alone as "boot this domain against
Lambda" would have silently rerouted Banking's every local
boot — every spec, every bin/console session — the moment
this landed. A domain opts in explicitly, the same way
persisted_by("PostgresEra") is never inferred from anything
else either.
165 166 167 168 169 170 171 |
# File 'lib/hecks/runtime/loader.rb', line 165 def self.dispatcher_for(registry) domain = registry.bluebooks.keys.first settings = registry.world(domain)&.for_verb("dispatched_by") || {} return Dispatcher.new(registry) unless settings[:adapter] == "Lambda" RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1")) end |
.run_boot_gates!(registry, directory) ⇒ Object
ADR 0031 — replaces two previously-hardcoded, unconditional calls
with a per-boot BootGates instance holding exactly the gates THIS
registry's own bound adapters have a capability for. Ordering is
preserved: era-checking (when a persistence plugin contributes one)
still runs before verify!, saga rehydration still runs after
(conservative — see SagaPersistence#rehydrate_sagas!'s own
comment).
ADR 0033 — this loader no longer names EraCheck, or any other
era-specific class, at all. Every LOADED persistence plugin
(Ports::Persistence.each_plugin — nothing here if nothing was
ever required) is asked to contribute its own :pre_verify/
:post_verify gates generically; :saga_rehydration is the one
gate core still registers directly, because ADR 0031 already
proved it's not era-specific.
106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/hecks/runtime/loader.rb', line 106 def self.run_boot_gates!(registry, directory) gates = BootGates.new Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) } check_compute_rules_backstop!(registry) gates.run!(:pre_verify, registry, directory) registry.verify! gates.register(:saga_rehydration, ->(reg, _dir) { reg.rehydrate_sagas! }, phase: :post_verify) if registry.hecksagons.each_key.any? { |domain| registry.saga_persistence(domain) != Ports::Persistence::NULL_SAGA_STORE } gates.run!(:post_verify, registry, directory) gates end |