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).
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.}" end nil end |
#fork ⇒ Object
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.
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 -----------------------------------------------------------
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.
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
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 |