Class: Hames::Context
- Inherits:
-
Object
- Object
- Hames::Context
- 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
-
#parent ⇒ Object
readonly
Returns the value of attribute parent.
Instance Method Summary collapse
- #[](key) ⇒ Object
-
#dispose! ⇒ Object
Dispose the whole context (child scopes call this when they end).
-
#dispose_owner!(owner) ⇒ Object
Dispose everything owned by
owner(a plugin id), reverse order. -
#effect(&block) ⇒ Object
Runs the block now; the block must return a disposer callable (or nil).
-
#emit(name, *args) ⇒ Object
emit: fire-and-forget, registration order, no return value.
-
#fork ⇒ Object
Child scope: sees parent services and listeners; its own registrations dispose when the scope ends (or when its owner unloads).
-
#initialize(parent: nil) ⇒ Context
constructor
A new instance of Context.
-
#listeners_for(name) ⇒ Object
Listeners visible to this context: parent chain first (registration order preserved within each context), respecting prepend within buckets.
- #method_missing(name, *args, &blk) ⇒ Object
-
#on(name, prepend: false, &block) ⇒ Object
Register a listener.
-
#parallel(name, *args) ⇒ Object
parallel: all listeners observe the event; awaited as a group.
-
#register_service(key, instance) ⇒ Object
-- services -----------------------------------------------------------.
- #respond_to_missing?(name, include_private = false) ⇒ Boolean
-
#serial(name, *args) ⇒ Object
serial: ordered, awaited, single-decision.
- #service?(key) ⇒ Boolean
-
#waterfall(name, *args, &base) ⇒ Object
waterfall: around-middleware.
- #with_owner(owner) ⇒ Object
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
#parent ⇒ Object (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 |
#fork ⇒ Object
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.
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 -----------------------------------------------------------
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
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
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 |