Class: Hames::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/hames/loader.rb

Overview

Mounts config rows into a context in dependency order derived from each plugin's inject list. Layers apply as ordered row lists; a later layer's row with an existing id replaces that row's config wholesale (never a deep merge); unknown ids append.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ctx = Context.new) ⇒ Loader

Returns a new instance of Loader.



79
80
81
82
83
# File 'lib/hames/loader.rb', line 79

def initialize(ctx = Context.new)
  @ctx = ctx
  @rows = {} # id => Row
  @mounted = {} # id => plugin instance
end

Instance Attribute Details

#ctxObject (readonly)

mounted is insertion-ordered by boot!, which mounts in dependency order — so its reverse is exact reverse-dependency order, and that is what a teardown wants: a consumer comes down before what it injects. Row order cannot stand in for it, since an inserted row's position in the tree need not match where its dependencies put it.



77
78
79
# File 'lib/hames/loader.rb', line 77

def ctx
  @ctx
end

#mountedObject (readonly)

mounted is insertion-ordered by boot!, which mounts in dependency order — so its reverse is exact reverse-dependency order, and that is what a teardown wants: a consumer comes down before what it injects. Row order cannot stand in for it, since an inserted row's position in the tree need not match where its dependencies put it.



77
78
79
# File 'lib/hames/loader.rb', line 77

def mounted
  @mounted
end

#rowsObject (readonly)

mounted is insertion-ordered by boot!, which mounts in dependency order — so its reverse is exact reverse-dependency order, and that is what a teardown wants: a consumer comes down before what it injects. Row order cannot stand in for it, since an inserted row's position in the tree need not match where its dependencies put it.



77
78
79
# File 'lib/hames/loader.rb', line 77

def rows
  @rows
end

Instance Method Details

#boot!Object

Mount all enabled rows. Plugins whose injected services are not yet present wait; repeated passes mount whatever has become satisfiable. No progress with plugins still pending => cycle / missing provider.



106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/hames/loader.rb', line 106

def boot!
  pending = @rows.values.reject(&:disabled).map { |r| [r, instantiate(r)] }
  until pending.empty?
    ready, pending = pending.partition { |(_r, pl)| satisfied?(pl) }
    if ready.empty?
      missing = pending.map { |(r, pl)| "#{r.id} (needs #{unmet(pl).join(', ')})" }
      raise CycleError, "cannot mount: #{missing.join('; ')}"
    end
    ready.each { |(r, pl)| mount(r, pl) }
  end
  ctx
end

#dump_configObject

Resolved tree, layer-agnostic view (for --dump-config).



160
161
162
# File 'lib/hames/loader.rb', line 160

def dump_config
  @rows.values.map { |r| { id: r.id, plugin: r.plugin.to_s, config: r.config, disabled: r.disabled } }
end

#layer(row_hashes) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/hames/loader.rb', line 85

def layer(row_hashes)
  row_hashes.each do |h|
    id = h.fetch(:id).to_s
    if (existing = @rows[id])
      # patch: wholesale config replacement; plugin class may also swap
      @rows[id] = Hames::Row.new(
        id: id,
        plugin: h[:plugin] || existing.plugin,
        config: h.key?(:config) ? (h[:config] || {}) : existing.config,
        disabled: h.key?(:disabled) ? h[:disabled] : existing.disabled
      )
    else
      @rows[id] = Row.build(h)
    end
  end
  self
end

#reconfigure!(id, config) ⇒ Object

Hot config swap — wholesale, like layering, never a merge. Replaces the row's config, swaps the mounted service's, invokes its reconfigure hook under with_owner(id) (so hook-registered effects are owned by the row, not left ownerless), and emits config/updated when the app vocabulary declares it (hames itself declares no events and stays app-agnostic). A raising hook rolls the row and the service's config back to what they were before the call and re-raises — the swap is atomic, never split-brain.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/hames/loader.rb', line 126

def reconfigure!(id, config)
  id = id.to_s
  old_row = @rows.fetch(id) { raise KeyError, "no row #{id.inspect}" }
  service = @mounted.fetch(id) { raise KeyError, "no mounted plugin #{id.inspect}" }
  old_config = old_row.config
  @rows[id] = Hames::Row.new(id: old_row.id, plugin: old_row.plugin, config: config, disabled: old_row.disabled)
  service.replace_config!(config)
  begin
    ctx.with_owner(id) { service.reconfigure(config) }
  rescue StandardError
    @rows[id] = old_row
    service.replace_config!(old_config)
    raise
  end
  ctx.emit("config/updated", id, config) if Hames.declared?("config/updated")
  config
end

#unload!(id) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/hames/loader.rb', line 144

def unload!(id)
  plugin = @mounted.fetch(id) { raise ArgumentError, "no mounted plugin #{id}" }
  # Owner effects are disposed even when stop raises, so a wedged stop hook
  # cannot strand this row's service registration and listeners; and the
  # @mounted entry is dropped only AFTER a clean teardown, so a stop that
  # raised keeps the row unloadable (a retry re-runs stop) rather than
  # vanishing behind a misleading "no mounted plugin".
  begin
    plugin.stop(ctx) if plugin.respond_to?(:stop)
  ensure
    ctx.dispose_owner!(id)
  end
  @mounted.delete(id)
end