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).



75
76
77
78
# File 'lib/hames/context.rb', line 75

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

#dispose_owner!(owner) ⇒ Object

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



66
67
68
69
70
71
72
# File 'lib/hames/context.rb', line 66

def dispose_owner!(owner)
  kept = []
  doomed = []
  @effects.each { |fr| (fr[0] == owner ? doomed : kept) << fr }
  @effects = kept
  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.



52
53
54
55
56
# File 'lib/hames/context.rb', line 52

def effect(&block)
  disposer = block.call
  @effects << [@owner, disposer] if disposer
  disposer
end

#emit(name, *args) ⇒ Object

emit: fire-and-forget, registration order, no return value.



104
105
106
107
108
# File 'lib/hames/context.rb', line 104

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

#forkObject

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



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

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.



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

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:



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

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)
  disposer = -> { bucket.delete(l) }
  @effects << [@owner, disposer]
  disposer
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.)



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

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.



140
141
142
143
144
145
146
147
# File 'lib/hames/context.rb', line 140

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.



114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/hames/context.rb', line 114

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



58
59
60
61
62
63
# File 'lib/hames/context.rb', line 58

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