Class: Hecks::Ports::Persistence::AppendOnly
- Inherits:
-
Object
- Object
- Hecks::Ports::Persistence::AppendOnly
- Defined in:
- lib/hecks/ports/persistence/append_only.rb
Overview
Makes append-before-projection a port invariant. Adapters retain control of their durable format, but every one must accept the same entry stream and materialize current state from it.
Instance Attribute Summary collapse
-
#adapter ⇒ Object
readonly
Returns the value of attribute adapter.
Instance Method Summary collapse
- #aggregate ⇒ Object
- #all(**opts) ⇒ Object
- #append(entry) ⇒ Object
- #atomic_put(instance, insert_only: false) ⇒ Object
- #capabilities ⇒ Object
- #count ⇒ Object
- #delete(id) ⇒ Object
- #entries ⇒ Object
-
#events ⇒ Object
NOT an endless `def events = ...
- #find(id) ⇒ Object
-
#initialize(adapter) ⇒ AppendOnly
constructor
A new instance of AppendOnly.
- #project(entry) ⇒ Object
- #query_read_model(domain, model, args, bluebook = nil) ⇒ Object
-
#record_event(event) ⇒ Object
NOT an endless `def record_event = ...
-
#recover! ⇒ Object
An append is durable before a projection is attempted.
- #reset! ⇒ Object
-
#save(instance, expected_version: nil) ⇒ Object
Returns an
Outcome, not a bareInstance— every call site (CommandInterpreter/EntityInterpreter's ownstep_save,RebuildSweep#refresh) reads it that way.
Constructor Details
#initialize(adapter) ⇒ AppendOnly
Returns a new instance of AppendOnly.
22 23 24 25 26 27 28 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 22 def initialize(adapter) @adapter = adapter required = %i[append project entries] missing = required.reject { |method| adapter.respond_to?(method) } raise Runtime::WiringError, "#{adapter.class} does not implement append-only persistence: #{missing.join(', ')}" unless missing.empty? end |
Instance Attribute Details
#adapter ⇒ Object (readonly)
Returns the value of attribute adapter.
18 19 20 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 18 def adapter @adapter end |
Instance Method Details
#aggregate ⇒ Object
20 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 20 def aggregate = @adapter.aggregate |
#all(**opts) ⇒ Object
31 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 31 def all(**opts) = @adapter.all(**opts) |
#append(entry) ⇒ Object
64 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 64 def append(entry) = @adapter.append(entry) |
#atomic_put(instance, insert_only: false) ⇒ Object
95 96 97 98 99 100 101 102 103 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 95 def atomic_put(instance, insert_only: false) unless capabilities.include?(:atomic_put) && @adapter.respond_to?(:atomic_put) raise Runtime::WiringError, "#{@adapter.class} advertises no atomic_put persistence capability" end entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup) status = @adapter.atomic_put(entry, insert_only: insert_only) Outcome.new(status: status, instance: instance) end |
#capabilities ⇒ Object
35 36 37 38 39 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 35 def capabilities return [] unless @adapter.respond_to?(:persistence_capabilities) Array(@adapter.persistence_capabilities).map(&:to_sym).freeze end |
#count ⇒ Object
32 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 32 def count = @adapter.count |
#delete(id) ⇒ Object
105 106 107 108 109 110 111 112 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 105 def delete(id) return false unless find(id) entry = Entry.new(operation: "delete", id: id.to_s, state: nil) append(entry) project(entry) true end |
#entries ⇒ Object
33 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 33 def entries = @adapter.entries |
#events ⇒ Object
NOT an endless def events = ... if ... — that modifier binds to
the WHOLE def, not just its body, so it evaluates against
@adapter while @adapter is still nil (class-body time,
before initialize ever runs) and silently skips defining the
method at all. Found live: nothing in this codebase called
AppendOnly#events before Memory got a reset! test that did.
53 54 55 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 53 def events @adapter.events if @adapter.respond_to?(:events) end |
#find(id) ⇒ Object
30 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 30 def find(id) = @adapter.find(id) |
#project(entry) ⇒ Object
65 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 65 def project(entry) = @adapter.project(entry) |
#query_read_model(domain, model, args, bluebook = nil) ⇒ Object
130 131 132 133 134 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 130 def query_read_model(domain, model, args, bluebook = nil) return unless @adapter.respond_to?(:query_read_model) @adapter.query_read_model(domain, model, args, bluebook) end |
#record_event(event) ⇒ Object
NOT an endless def record_event = ... if ... — same gotcha as
events above, and it bit for real here: this guard evaluated
against @adapter at class-body time (nil, always false), so
record_event was never defined at all. emission.rb's own
repository.record_event(event) if repository.respond_to?(:record_event)
therefore never fired for any adapter, ever — every declared
emits was computed and reported in registry.event_log (an
in-process array, gone at exit) but never durably recorded.
Caught because a live tail of a domain's own persisted events
found nothing to tail. sqlite_spec.rb/postgres_spec.rb/
postgres_era_spec.rb all call adapter.record_event directly,
bypassing this wrapper — which is exactly why no spec noticed.
126 127 128 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 126 def record_event(event) @adapter.record_event(event) if @adapter.respond_to?(:record_event) end |
#recover! ⇒ Object
An append is durable before a projection is attempted. Replaying the log restores a snapshot/table after a crash in that small window.
59 60 61 62 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 59 def recover! entries.each { |entry| project(entry) } self end |
#reset! ⇒ Object
41 42 43 44 45 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 41 def reset! raise Runtime::WiringError, "append-only adapter cannot reset" unless @adapter.respond_to?(:reset!) @adapter.reset! end |
#save(instance, expected_version: nil) ⇒ Object
Returns an Outcome, not a bare Instance — every call site
(CommandInterpreter/EntityInterpreter's own step_save,
RebuildSweep#refresh) reads it that way.
expected_version: requests optimistic-concurrency CAS — commit
only if the stored record's version still matches what THIS
instance was read at. It is nil both when a caller explicitly
doesn't want CAS (RebuildSweep#refresh's own projection-field
touch-up, which has no given to protect) and when the instance
is brand new (never read from storage, so instance.version is
nil) — both cases fall through to the plain, unconditional
project(entry) below, byte-for-byte today's behavior. Only an
adapter that both receives a non-nil expected_version AND
declares :optimistic_concurrency gets CAS treatment; every
other adapter/call site is unaffected.
82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/hecks/ports/persistence/append_only.rb', line 82 def save(instance, expected_version: nil) entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup) append(entry) if expected_version && capabilities.include?(:optimistic_concurrency) saved = @adapter.project(entry, expected_version: expected_version) return Outcome.new(status: :stale, instance: instance) if saved.nil? return Outcome.new(status: :saved, instance: saved) end saved = @adapter.project(entry) Outcome.new(status: :saved, instance: saved || instance) end |