Class: Hames::Context

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

Overview

A Context is a repository of services and an event bus. Plugins mount into a context, claim service keys, register listeners, and install reversible effects. Forked contexts inherit parent services and listeners; their own registrations dispose independently (the per-agent scope primitive).

Defined Under Namespace

Classes: Listener

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent: nil) ⇒ Context

Returns a new instance of Context.



13
14
15
16
17
18
19
# File 'lib/hames/context.rb', line 13

def initialize(parent: nil)
  @parent    = parent
  @services  = {}
  @listeners = Hash.new { |h, k| h[k] = [] }
  @effects   = [] # frames: [owner, disposer] in registration order
  @owner     = nil # plugin id currently mounting (set by Loader)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &blk) ⇒ Object



40
41
42
# File 'lib/hames/context.rb', line 40

def method_missing(name, *args, &blk)
  service?(name) && args.empty? && blk.nil? ? self[name] : super
end

Instance Attribute Details

#parentObject (readonly)

Returns the value of attribute parent.



11
12
13
# File 'lib/hames/context.rb', line 11

def parent
  @parent
end

Instance Method Details

#[](key) ⇒ Object



32
33
34
35
36
# File 'lib/hames/context.rb', line 32

def [](key)
  key = key.to_sym
  @services[key] || parent&.[](key) ||
    raise(ServiceMissingError, "no service registered at ctx[:#{key}]")
end

#dispose!Object

Dispose the whole context (child scopes call this when they end).



97
98
99
100
# File 'lib/hames/context.rb', line 97

def dispose!
  @effects.dup.reverse_each { |(_o, d)| d.call }
  @effects.clear
end

#dispose_owner!(owner) ⇒ Object

Dispose everything owned by owner (a plugin id), reverse order.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/hames/context.rb', line 84

def dispose_owner!(owner)
  kept = []
  doomed = []
  @effects.each { |fr| (fr[0] == owner ? doomed : kept) << fr }
  @effects = kept
  # each wrapped disposer re-scans @effects for a frame that the
  # partition above already removed — an O(doomed·kept) miss. Negligible
  # at current roster sizes; if owner disposal ever gets hot, carry the
  # original disposer on the frame and call it directly here.
  doomed.reverse_each { |(_o, d)| d.call }
end

#effect(&block) ⇒ Object

Runs the block now; the block must return a disposer callable (or nil). Disposal happens in reverse registration order, per owner, on unload. The disposer handed back is self-removing and idempotent: calling it runs the teardown once and drops its own entry from @effects, so a long-lived context does not pin every disposed registration (and whatever its closure captured). The done flag lives on the frame (not presence in @effects) because dispose_owner! removes frames from the real teardown there.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/hames/context.rb', line 59

def effect(&block)
  disposer = block.call
  return disposer unless disposer

  frame = [@owner, nil, false]
  wrapped = lambda do
    next if frame[2]

    frame[2] = true
    @effects.delete(frame)
    disposer.call
  end
  frame[1] = wrapped
  @effects << frame
  wrapped
end

#emit(name, *args) ⇒ Object

emit: fire-and-forget, registration order, no return value. Listener failures are isolated (warned, not raised): by the time listeners run, the producer's fact is already committed — a durable append emits after the store write — so a consumer bug must not un-happen it. The other modes raise through: their results are load-bearing.



128
129
130
131
132
133
134
135
136
# File 'lib/hames/context.rb', line 128

def emit(name, *args)
  Hames.assert_mode!(name, :emit)
  listeners_for(name).each do |l|
    l.block.call(*args)
  rescue StandardError => e
    warn "hames: emit(#{name}): listener isolated: #{e.class}: #{e.message}"
  end
  nil
end

#forkObject

Child scope: sees parent services and listeners; its own registrations dispose when the scope ends (or when its owner unloads).



181
# File 'lib/hames/context.rb', line 181

def fork = Context.new(parent: self)

#listeners_for(name) ⇒ Object

Listeners visible to this context: parent chain first (registration order preserved within each context), respecting prepend within buckets.



118
119
120
121
# File 'lib/hames/context.rb', line 118

def listeners_for(name)
  own = @listeners[name.to_s]
  parent ? parent.listeners_for(name) + own : own.dup
end

#on(name, prepend: false, &block) ⇒ Object

Register a listener. Mode is validated against the event declaration. Returns a disposer and records it as an effect of the current owner.

Raises:



106
107
108
109
110
111
112
113
114
# File 'lib/hames/context.rb', line 106

def on(name, prepend: false, &block)
  name = name.to_s
  raise ContractError, "listener for undeclared event #{name}" unless Hames.declared?(name)

  l = Listener.new(name:, block:, prepend:, owner: @owner)
  bucket = @listeners[name]
  prepend ? bucket.unshift(l) : bucket.push(l)
  effect { -> { bucket.delete(l) } }
end

#parallel(name, *args) ⇒ Object

parallel: all listeners observe the event; awaited as a group. Without a reactor this runs each in sequence but preserves the contract that dispatch completes only when every listener has. (Under terret's async runtime this maps onto an Async barrier.)



160
161
162
163
164
# File 'lib/hames/context.rb', line 160

def parallel(name, *args)
  Hames.assert_mode!(name, :parallel)
  listeners_for(name).each { |l| l.block.call(*args) }
  nil
end

#register_service(key, instance) ⇒ Object

-- services -----------------------------------------------------------

Raises:



23
24
25
26
27
28
29
30
# File 'lib/hames/context.rb', line 23

def register_service(key, instance)
  key = key.to_sym
  raise ContractError, "service #{key} already registered" if @services.key?(key)

  @services[key] = instance
  effect { -> { @services.delete(key) } }
  instance
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/hames/context.rb', line 44

def respond_to_missing?(name, include_private = false)
  service?(name) || super
end

#serial(name, *args) ⇒ Object

serial: ordered, awaited, single-decision. The first non-nil listener return value wins and stops dispatch.



168
169
170
171
172
173
174
175
# File 'lib/hames/context.rb', line 168

def serial(name, *args)
  Hames.assert_mode!(name, :serial)
  listeners_for(name).each do |l|
    result = l.block.call(*args)
    return result unless result.nil?
  end
  nil
end

#service?(key) ⇒ Boolean

Returns:

  • (Boolean)


38
# File 'lib/hames/context.rb', line 38

def service?(key) = @services.key?(key.to_sym) || parent&.service?(key) || false

#waterfall(name, *args, &base) ⇒ Object

waterfall: around-middleware. Each listener receives (*args, next_). Calling next_.(payload) delegates; returning without calling next_ short-circuits. The innermost next_ returns its (possibly rewritten) payload — or calls the base block if one is given.



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/hames/context.rb', line 142

def waterfall(name, *args, &base)
  Hames.assert_mode!(name, :waterfall)
  chain = listeners_for(name)
  invoke = lambda do |i, current_args|
    if i >= chain.length
      base ? base.call(*current_args) : current_args.first
    else
      next_ = ->(*rewritten) { invoke.call(i + 1, rewritten.empty? ? current_args : rewritten) }
      chain[i].block.call(*current_args, next_)
    end
  end
  invoke.call(0, args)
end

#with_owner(owner) ⇒ Object



76
77
78
79
80
81
# File 'lib/hames/context.rb', line 76

def with_owner(owner)
  prev, @owner = @owner, owner
  yield
ensure
  @owner = prev
end