Module: Hecks::Runtime::RebuildSweep

Defined in:
lib/hecks/runtime/rebuild_sweep.rb

Overview

THE OUT-OF-BAND HALF OF projects (S12, ADR 0025 — "Consistency across aggregate boundaries"). A projected field is never written by the command that reads it — nothing at dispatch time takes a live cross-aggregate read the way CommandRules::References #dereference still does — so this is the one place a projected field's value actually gets copied over: walk every record of the OWNING aggregate, resolve each of its own projected_fields through the reference it names, and save the local copy.

EXPLICIT AND CALLABLE, NOT AUTOMATIC — no on-boot detection of a freshly-declared projects with no held-era precedent, no generated Policy#for_each reaction keeping it live in real time as the target changes. Both are real extensions this same ADR section describes; both are deliberately deferred. This is the mechanism a caller reaches for by hand — bin/rebuild Domain::Aggregate, a scheduled job, whatever the deployment needs — proven to work end to end before either automatic trigger is built on top of it.

NEEDS NO NEW ADAPTER CAPABILITY. find/all/save are the same three primitives every real adapter already answers identically (Ports::Persistence::AppendOnly#save — append, then project, the same for Memory/Postgres/SQLite/D1/Heki) — confirmed by direct reading before this was built, not assumed.

Class Method Summary collapse

Class Method Details

.call(registry, domain, aggregate) ⇒ Object

One aggregate's own projected fields, refreshed across every record it holds. Returns how many records actually changed — save only runs when a projected value would differ from what is already stored, so re-running a sweep with nothing having moved on the target side touches the append log not at all.



35
36
37
38
39
40
# File 'lib/hecks/runtime/rebuild_sweep.rb', line 35

def call(registry, domain, aggregate)
  return 0 if aggregate.projected_fields.empty?

  repository = registry.repository(domain, aggregate)
  repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
end

.refresh(registry, domain, aggregate, record, repository) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/hecks/runtime/rebuild_sweep.rb', line 42

def refresh(registry, domain, aggregate, record, repository)
  changed = false

  aggregate.projected_fields.each do |field|
    value = remote_value(registry, domain, aggregate, record, field)
    next if value.nil?
    next if record.key?(field.name) && record[field.name] == value

    record[field.name] = value
    changed = true
  end

  repository.save(record) if changed
  changed
end

.remote_value(registry, domain, aggregate, record, field) ⇒ Object

nil when the reference itself does not resolve in this chapter (a cross-domain target left "unfollowed" the same way References#dereference already leaves one) or when the record names no target at all — an optional reference nobody set.



62
63
64
65
66
67
68
69
70
71
# File 'lib/hecks/runtime/rebuild_sweep.rb', line 62

def remote_value(registry, domain, aggregate, record, field)
  target = aggregate.attribute(field.reference)&.type&.resolve
  return nil unless target

  id = record[field.reference]
  return nil if id.nil?

  remote = registry.repository(domain, target).find(id.to_s)
  remote&.[](field.remote_field)
end