Class: Cordis::Context

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

Overview

Thin shell: root bootstrap, service access (the paper's coeffect context), and delegation to the plugin/event entry points. Breaking change (round 2): extend now matches upstream semantics — it creates a "same root, different fiber" view of the ctx, not a new disposal scope; new scopes always come from ctx.plugin.

Defined Under Namespace

Classes: Impl

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeContext

Returns a new instance of Context.



14
15
16
17
18
19
20
21
22
# File 'lib/cordis/context.rb', line 14

def initialize
  @root = self
  @services = {} # isolate key => Impl (default key is the name itself)
  @isolate = {}  # name => isolation label; absent = the shared default realm
  @intercept = {} # name => per-caller config (copy-on-write, chain pre-merged)
  @fiber = Fiber.new(self) # root fiber: always active, dispose = restart
  @registry = Registry.new(self)
  @events = Events.new(self)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

Service access: walk the fiber parent chain looking at snapshots (error messages match upstream).



154
155
156
157
158
# File 'lib/cordis/context.rb', line 154

def method_missing(name, *args, &block)
  return super unless args.empty? && block.nil?

  resolve_service(name)
end

Instance Attribute Details

#eventsObject (readonly)

Returns the value of attribute events.



12
13
14
# File 'lib/cordis/context.rb', line 12

def events
  @events
end

#fiberObject (readonly)

Returns the value of attribute fiber.



12
13
14
# File 'lib/cordis/context.rb', line 12

def fiber
  @fiber
end

#registryObject (readonly)

Returns the value of attribute registry.



12
13
14
# File 'lib/cordis/context.rb', line 12

def registry
  @registry
end

#rootObject (readonly)

Returns the value of attribute root.



12
13
14
# File 'lib/cordis/context.rb', line 12

def root
  @root
end

#servicesObject (readonly)

Returns the value of attribute services.



12
13
14
# File 'lib/cordis/context.rb', line 12

def services
  @services
end

Instance Method Details

#bail(name) ⇒ Object



77
# File 'lib/cordis/context.rb', line 77

def bail(name, *) = @root.events.bail(name, *)

#effect(label = nil) ⇒ Object

-- effect / plugin --



65
# File 'lib/cordis/context.rb', line 65

def effect(label = nil, &) = @fiber.effect(label, &)

#emit(name) ⇒ Object



76
# File 'lib/cordis/context.rb', line 76

def emit(name, *) = @root.events.emit(name, *)

#extend(fiber: @fiber) ⇒ Object

Internal: a ctx view bound to another fiber (sharing root/registry/events/services).



25
26
27
28
29
# File 'lib/cordis/context.rb', line 25

def extend(fiber: @fiber)
  ctx = dup
  ctx.instance_variable_set(:@fiber, fiber)
  ctx
end

#get(name) ⇒ Object

Strict: the provider fiber must be ACTIVE to be visible (a service whose provider is still loading is invisible to dependents).



116
117
118
119
120
121
# File 'lib/cordis/context.rb', line 116

def get(name)
  impl = @root.services[isolate_key(name.to_sym)]
  return nil unless impl && impl.fiber.state == :active

  impl.value
end

#inject(deps, &block) ⇒ Object



68
69
70
# File 'lib/cordis/context.rb', line 68

def inject(deps, &block)
  plugin({ inject: deps, apply: block, name: 'inject' })
end

#intercept(name, config) ⇒ Object

A ctx view carrying per-caller config for service name (upstream ctx.intercept). Nested intercepts merge, inner overriding outer (Hash configs only; anything else replaces).



42
43
44
45
46
47
48
49
# File 'lib/cordis/context.rb', line 42

def intercept(name, config)
  name = name.to_sym
  old = @intercept[name]
  merged = old.is_a?(Hash) && config.is_a?(Hash) ? old.merge(config) : config
  ctx = extend
  ctx.instance_variable_set(:@intercept, @intercept.merge(name => merged))
  ctx
end

#isolate(name, label = nil) ⇒ Object

A ctx view where service name lives in its own realm: provides/injects through this view no longer see (or are seen by) the default realm. Pass the same label to two isolate calls to share one realm between them.



34
35
36
37
38
# File 'lib/cordis/context.rb', line 34

def isolate(name, label = nil)
  ctx = extend
  ctx.instance_variable_set(:@isolate, @isolate.merge(name.to_sym => label || Object.new))
  ctx
end

#isolate_key(name) ⇒ Object

The store key for name in this ctx's realm.



61
# File 'lib/cordis/context.rb', line 61

def isolate_key(name) = @isolate[name] || name

#notify(pairs) ⇒ Object

Dependency-graph update (fully synchronous): linear scan over all fibers, re-checking satisfaction and recomputing epochs. Only fibers whose ctx resolves the name to the same isolate key are touched. Takes { name => key } pairs and returns the touched fibers (provide's teardown uses the list to wait for dependents).



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/cordis/context.rb', line 136

def notify(pairs)
  touched = []
  @registry.runtimes.each do |runtime|
    runtime.fibers.each do |fib|
      hits = pairs.select { |n, key| fib.inject.key?(n) && fib.ctx.isolate_key(n) == key }
      next if hits.empty?

      hits.each_key { |n| fib.check_impl(n) }
      fib.refresh
      touched << fib
    end
  end
  pairs.each_key { |n| @events.emit('internal/service', n) }
  touched
end

#on(name, prepend: false) ⇒ Object

-- events --



74
# File 'lib/cordis/context.rb', line 74

def on(name, prepend: false, &) = @root.events.register(self, name, prepend: prepend, &)

#once(name, prepend: false) ⇒ Object



75
# File 'lib/cordis/context.rb', line 75

def once(name, prepend: false, &) = @root.events.once(self, name, prepend: prepend, &)

#parallel(name) ⇒ Object



79
# File 'lib/cordis/context.rb', line 79

def parallel(name, *) = @root.events.parallel(name, *)

#plugin(plugin, config = nil) ⇒ Object



66
# File 'lib/cordis/context.rb', line 66

def plugin(plugin, config = nil) = @root.registry.plugin(self, plugin, config)

#provide(name, value = nil) ⇒ Object

Providing a service is itself a revertible effect. Teardown ordering guarantee: remove from the store -> notify -> wait for every dependent to finish unloading -> only then delete the provider's self-visible entry (so dependents' disposers can still reach the service during their own teardown).



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/cordis/context.rb', line 88

def provide(name, value = nil)
  name = name.to_sym
  key = isolate_key(name)
  owner = @fiber
  @fiber.effect("ctx.provide(#{name.inspect})") do
    raise ServiceError, "service #{name.inspect} has already been registered" if @root.services.key?(key)

    impl = Impl.new(name, owner, value)
    @root.services[key] = impl
    owner.store[name] = impl # immediately visible to the provider itself
    owner.provided[name] = key
    @root.notify({ name => key }) if owner.state == :active
    lambda do
      @root.services.delete(key)
      dependents = @root.notify({ name => key })
      dependents.each do |dep|
        dep.await
      rescue StandardError
        nil # allSettled semantics: one dependent failing to unload never blocks the rest
      end
      owner.store&.delete(name)
      owner.provided.delete(name)
    end
  end
end

#resolve_config(name, base = nil) ⇒ Object

Effective intercept config for name as seen from this ctx, merged over base (upstream Service, minus the Config schema merge).



53
54
55
56
57
58
# File 'lib/cordis/context.rb', line 53

def resolve_config(name, base = nil)
  config = @intercept[name.to_sym]
  return base if config.nil?

  base.is_a?(Hash) && config.is_a?(Hash) ? base.merge(config) : config
end

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

Returns:

  • (Boolean)


160
161
162
# File 'lib/cordis/context.rb', line 160

def respond_to_missing?(name, include_private = false)
  @root.services.key?(isolate_key(name)) || super
end

#serial(name) ⇒ Object



78
# File 'lib/cordis/context.rb', line 78

def serial(name, *) = @root.events.serial(name, *)

#set(name, value) ⇒ Object

Raises:



123
124
125
126
127
128
129
130
# File 'lib/cordis/context.rb', line 123

def set(name, value)
  name = name.to_sym
  impl = @root.services[isolate_key(name)]
  raise ServiceError, %(cannot set property "#{name}" without provide) unless impl
  raise ServiceError, %(cannot set property "#{name}" in multiple fibers) unless impl.fiber.equal?(@fiber)

  impl.value = value
end

#waterfall(name) ⇒ Object



80
# File 'lib/cordis/context.rb', line 80

def waterfall(name, *, &) = @root.events.waterfall(name, *, &)