Module: Kobako::Transport::Dispatcher
- Defined in:
- lib/kobako/transport/dispatcher.rb,
sig/kobako/transport/dispatcher.rbs
Overview
Pure-function dispatcher for guest-initiated Calls. The native side
has already decoded the core envelope, so this resolves the target
through the per-invocation path resolver (the Context, whose
#lookup layers per-invocation providers over the static bindings)
or Catalog::Handles, decodes only the payload, invokes the method,
and answers [ok, bytes] — which the native side puts on the
Reply's ok or fault arm. It never raises.
The module is stateless — all mutable state is threaded through
arguments so Dispatcher has no instance variables and no side
effects beyond mutating the Catalog::Handles via alloc when a
non-wire-representable return value must be wrapped.
Defined Under Namespace
Classes: UndefinedTargetError
Constant Summary collapse
- BREAK_THROW =
Throw tag for the Yielder's break unwind back to the dispatcher's
catchframe.private_constantis a convention boundary — not a defence. :__kobako_break__
Class Method Summary collapse
-
.caught_fault(error) ⇒ Object
Map an error caught at the dispatch boundary to the message and the category the native side frames into the Reply's fault arm.
-
.dispatch(call, resolver, handler, yield_to_guest) ⇒ Object
Answer a single routed Call with
[ok, bytes], which the native side puts on the Reply's ok or fault arm. -
.encode_ok(value, handler) ⇒ Object
Encode
valueas the body of a Reply's ok arm — the value alone, since the envelope's tag already carries the success. -
.fault(type, message) ⇒ Object
messagefolds to UTF-8 first: Ruby core builds some exception messages as ASCII-8BIT (the arity ArgumentError, for one), and the envelope requires UTF-8 of the text fields it frames. -
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch
methodontarget. -
.reject_unreachable!(target, name) ⇒ Object
Guard the
public_sendbelow: Reflection decides what counts as Service behaviour on this target, and its refusal reason becomes the guest'sundefinedfault. -
.require_live_object!(id, handler) ⇒ Object
Resolve
idthrough the Catalog::Handles. -
.resolve_arg(value, handler) ⇒ Object
Resolve every Kobako::Handle in an argument — bare or nested in an Array / Hash — back to its host object before the dispatch reaches
public_send, symmetric with the guest→host return path. -
.resolve_call_args(arguments, handler, carried_handle) ⇒ Object
Resolve positional and keyword arguments off the decoded payload in one step.
- .resolve_path(path, resolver) ⇒ Object
-
.resolve_target(target, resolver, handler) ⇒ Object
Resolve a Call target to the Ruby object the path
resolver(or Catalog::Handles) holds. -
.run(call, resolver, handler, yielder) ⇒ Object
Decode the payload, resolve the receiver, and run the method inside the
catchframe a guestbreakunwinds to. -
.wrap_as_handle(value, handler) ⇒ Object
Allocate
valuein the Sandbox's Catalog::Handles and return aHandlethat the wire codec can carry.
Instance Method Summary collapse
- #self?.caught_fault ⇒ [String, String]
- #self?.dispatch ⇒ [bool, String, String?]
- #self?.encode_ok ⇒ String
- #self?.fault ⇒ [String, String]
- #self?.invoke ⇒ Object
- #self?.reject_unreachable! ⇒ void
- #self?.require_live_object! ⇒ Object
- #self?.resolve_arg ⇒ Object
- #self?.resolve_call_args ⇒ [Array[untyped], Hash[Symbol, untyped]]
- #self?.resolve_path ⇒ Object
- #self?.resolve_target ⇒ Object
- #self?.run ⇒ Object
- #self?.wrap_as_handle ⇒ Kobako::Handle
Class Method Details
.caught_fault(error) ⇒ Object
Map an error caught at the dispatch boundary to the message and the
category the native side frames into the Reply's fault arm. error
is the StandardError caught by #dispatch's rescue; the category
tells the guest which kind of failure it was so it can raise the
matching proxy-side error.
98 99 100 101 102 103 104 105 106 |
# File 'lib/kobako/transport/dispatcher.rb', line 98 def caught_fault(error) case error when Kobako::Codec::Error then fault("runtime", "Sandbox received a malformed request: #{error.}") when UndefinedTargetError then fault("undefined", error.) when ArgumentError then fault("argument", error.) else fault("runtime", "#{error.class}: #{error.}") end end |
.dispatch(call, resolver, handler, yield_to_guest) ⇒ Object
Answer a single routed Call with [ok, bytes], which the native
side puts on the Reply's ok or fault arm. Invoked from the
per-invocation dispatch Proc that
Kobako::Context hands to Runtime#eval / #run; resolver,
handler, and yield_to_guest are captured in that Proc's
closure so the Dispatcher stays stateless and neither the resolver
nor the Context needs to publish accessors for the per-invocation
Catalog::Handles or Runtime. yield_to_guest is a String → String callable
(the ext's per-dispatch Kobako::Runtime::GuestYielder) used only
when the Call carries block_given: true. Never raises — every
failure path takes the fault arm instead, so the guest sees a
transport error rather than a wasm trap.
The decode runs inside Codec.track_handles so #resolve_call_args
can skip the argument walk when no Capability Handle crossed the
wire.
55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/kobako/transport/dispatcher.rb', line 55 def dispatch(call, resolver, handler, yield_to_guest) yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if call.block_given [true, encode_ok(run(call, resolver, handler, yielder), handler), nil] # : [bool, String, String?] # StandardError is the boundary by intent: a Service method's # application fault folds into a guest-rescuable fault, while a # host-process failure (NoMemoryError, SignalException, a bare Exception) # stays uncaught and traps the invocation rather than being masked as a # rescuable fault. rescue StandardError => e [false, *caught_fault(e)] # : [bool, String, String?] ensure yielder&.invalidate! end |
.encode_ok(value, handler) ⇒ Object
Encode value as the body of a Reply's ok arm — the value alone,
since the envelope's tag already carries the success. A value that
is not wire-representable per the codec's type mapping raises
UnsupportedTypeError; the rescue routes it through the
Catalog::Handles via #wrap_as_handle and re-encodes with the
Capability Handle in place. The happy path encodes exactly once.
185 186 187 188 189 |
# File 'lib/kobako/transport/dispatcher.rb', line 185 def encode_ok(value, handler) Kobako::Codec::Encoder.encode(value) rescue Kobako::Codec::UnsupportedTypeError encode_ok(wrap_as_handle(value, handler), handler) end |
.fault(type, message) ⇒ Object
message folds to UTF-8 first: Ruby core builds some exception
messages as ASCII-8BIT (the arity ArgumentError, for one), and
the envelope requires UTF-8 of the text fields it frames.
201 202 203 |
# File 'lib/kobako/transport/dispatcher.rb', line 201 def fault(type, ) [.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), type] # : [String, String] end |
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch method on target. kwargs is already Symbol-keyed
(the Payload::Arguments invariant pins it). The empty-kwargs branch omits
the ** splat so Ruby 3.x's strict kwargs separation does not
reject calls to no-kwarg methods when the wire carries the
uniform empty-map shape.
yielder is the host-side Yielder materialised when the guest
call site supplied a block; its Yielder#to_proc
rides the &block slot. &nil is a no-op block argument in Ruby,
so the same call site handles both cases without an explicit
conditional.
119 120 121 122 123 124 125 126 127 128 |
# File 'lib/kobako/transport/dispatcher.rb', line 119 def invoke(target, method, args, kwargs, yielder = nil) name = method.to_sym reject_unreachable!(target, name) block = yielder&.to_proc if kwargs.empty? target.public_send(name, *args, &block) else target.public_send(name, *args, **kwargs, &block) end end |
.reject_unreachable!(target, name) ⇒ Object
Guard the public_send below: Reflection decides what counts as
Service behaviour on this target, and its refusal reason becomes
the guest's undefined fault. Both the ambient-surface floor and
the target's own narrowing predicate answer through it, so a
rejected name discloses nothing about which of the two refused.
135 136 137 138 |
# File 'lib/kobako/transport/dispatcher.rb', line 135 def reject_unreachable!(target, name) reason = Reflection.refusal(target, name) raise UndefinedTargetError, reason if reason end |
.require_live_object!(id, handler) ⇒ Object
Resolve id through the Catalog::Handles. An unknown id
surfaces as UndefinedTargetError.
173 174 175 176 177 |
# File 'lib/kobako/transport/dispatcher.rb', line 173 def require_live_object!(id, handler) handler.fetch(id) rescue Kobako::SandboxError => e raise UndefinedTargetError, e. end |
.resolve_arg(value, handler) ⇒ Object
Resolve every Kobako::Handle in an argument — bare or nested in an
Array / Hash — back to its host object before the dispatch reaches
public_send, symmetric with the guest→host return path. A Handle id
with no live entry surfaces as an unrecognized target.
144 145 146 147 148 |
# File 'lib/kobako/transport/dispatcher.rb', line 144 def resolve_arg(value, handler) Kobako::Codec::HandleWalk.deep_restore(value, handler) rescue Kobako::SandboxError => e raise UndefinedTargetError, e. end |
.resolve_call_args(arguments, handler, carried_handle) ⇒ Object
Resolve positional and keyword arguments off the decoded payload in
one step. carried_handle reports whether the decode carried any
Capability Handle; when it did not, every argument resolves to
itself, so the decoded values pass straight through and the walk is
skipped entirely. Otherwise both go through #resolve_arg so Handles
round-trip back to the host-side Ruby object before the call reaches
public_send.
86 87 88 89 90 91 |
# File 'lib/kobako/transport/dispatcher.rb', line 86 def resolve_call_args(arguments, handler, carried_handle) return [arguments.args, arguments.kwargs] unless carried_handle [arguments.args.map { |v| resolve_arg(v, handler) }, arguments.kwargs.transform_values { |v| resolve_arg(v, handler) }] end |
.resolve_path(path, resolver) ⇒ Object
165 166 167 168 169 |
# File 'lib/kobako/transport/dispatcher.rb', line 165 def resolve_path(path, resolver) resolver.lookup(path) rescue KeyError => e raise UndefinedTargetError, e. end |
.resolve_target(target, resolver, handler) ⇒ Object
Resolve a Call target to the Ruby object the path resolver (or
Catalog::Handles) holds. The native side already discriminated the
two forms off the core envelope's kind tag: a String is a bound
constant's path, an Integer is a Capability Handle id. No
else-branch is needed — the envelope layer is the system boundary
that enforces the invariant.
156 157 158 159 160 161 162 163 |
# File 'lib/kobako/transport/dispatcher.rb', line 156 def resolve_target(target, resolver, handler) case target when String resolve_path(target, resolver) when Integer require_live_object!(target, handler) end end |
.run(call, resolver, handler, yielder) ⇒ Object
Decode the payload, resolve the receiver, and run the method inside
the catch frame a guest break unwinds to. Split from #dispatch
so the reply-shaping and the failure boundary stay one glance wide.
72 73 74 75 76 77 |
# File 'lib/kobako/transport/dispatcher.rb', line 72 def run(call, resolver, handler, yielder) arguments, carried_handle = Kobako::Codec.track_handles { Payload::Arguments.decode(call.payload) } receiver = resolve_target(call.target, resolver, handler) args, kwargs = resolve_call_args(arguments, handler, carried_handle) catch(BREAK_THROW) { invoke(receiver, call.method_name, args, kwargs, yielder) } end |
.wrap_as_handle(value, handler) ⇒ Object
Allocate value in the Sandbox's Catalog::Handles and return a Handle
that the wire codec can carry. Used as the fallback path of
#encode_ok when value has no wire representation.
194 195 196 |
# File 'lib/kobako/transport/dispatcher.rb', line 194 def wrap_as_handle(value, handler) handler.alloc(value) end |
Instance Method Details
#self?.caught_fault ⇒ [String, String]
18 |
# File 'sig/kobako/transport/dispatcher.rbs', line 18
def self?.caught_fault: (StandardError error) -> [String, String]
|
#self?.dispatch ⇒ [bool, String, String?]
12 |
# File 'sig/kobako/transport/dispatcher.rbs', line 12
def self?.dispatch: (Kobako::Transport::Call call, Kobako::Transport::_ServiceRegistry resolver, Kobako::Codec::_HandleTable handler, Kobako::Transport::_GuestYielder yield_to_guest) -> [bool, String, String?]
|
#self?.encode_ok ⇒ String
33 |
# File 'sig/kobako/transport/dispatcher.rbs', line 33
def self?.encode_ok: (untyped value, Kobako::Codec::_HandleTable handler) -> String
|
#self?.fault ⇒ [String, String]
37 |
# File 'sig/kobako/transport/dispatcher.rbs', line 37
def self?.fault: (String type, String message) -> [String, String]
|
#self?.invoke ⇒ Object
20 |
# File 'sig/kobako/transport/dispatcher.rbs', line 20
def self?.invoke: (untyped target, String method, Array[untyped] args, Hash[Symbol, untyped] kwargs, ?Kobako::Transport::Yielder? yielder) -> untyped
|
#self?.reject_unreachable! ⇒ void
This method returns an undefined value.
22 |
# File 'sig/kobako/transport/dispatcher.rbs', line 22
def self?.reject_unreachable!: (untyped target, Symbol name) -> void
|
#self?.require_live_object! ⇒ Object
31 |
# File 'sig/kobako/transport/dispatcher.rbs', line 31
def self?.require_live_object!: (Integer id, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.resolve_arg ⇒ Object
25 |
# File 'sig/kobako/transport/dispatcher.rbs', line 25
def self?.resolve_arg: (untyped value, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.resolve_call_args ⇒ [Array[untyped], Hash[Symbol, untyped]]
16 |
# File 'sig/kobako/transport/dispatcher.rbs', line 16
def self?.resolve_call_args: (Kobako::Payload::Arguments arguments, Kobako::Codec::_HandleTable handler, bool carried_handle) -> [Array[untyped], Hash[Symbol, untyped]]
|
#self?.resolve_path ⇒ Object
29 |
# File 'sig/kobako/transport/dispatcher.rbs', line 29
def self?.resolve_path: (String path, Kobako::Transport::_ServiceRegistry resolver) -> untyped
|
#self?.resolve_target ⇒ Object
27 |
# File 'sig/kobako/transport/dispatcher.rbs', line 27
def self?.resolve_target: (String | Integer target, Kobako::Transport::_ServiceRegistry resolver, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.run ⇒ Object
14 |
# File 'sig/kobako/transport/dispatcher.rbs', line 14
def self?.run: (Kobako::Transport::Call call, Kobako::Transport::_ServiceRegistry resolver, Kobako::Codec::_HandleTable handler, Kobako::Transport::Yielder? yielder) -> untyped
|
#self?.wrap_as_handle ⇒ Kobako::Handle
35 |
# File 'sig/kobako/transport/dispatcher.rbs', line 35
def self?.wrap_as_handle: (untyped value, Kobako::Codec::_HandleTable handler) -> Kobako::Handle
|