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, UnreadableRequestError
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__- OWN_FAULTS =
The category kobako's own refusals answer under, keyed by the class each is raised as and ordered most specific first. A class absent here is the Service's own exception, which answers under
runtimewearing the<class>: <message>shape that says so.Kobako::Codec::Erroris the floor rather than a path of its own: a codec fault reaching the boundary unnamed is the exchange failing, and must not be dressed as something a Service raised. { HandleExhaustedError => "internal", UndefinedTargetError => "undefined", ArgumentError => "argument", YieldValueError => "runtime", Kobako::Codec::Error => "internal", Kobako::SandboxError => "runtime" }.freeze
Class Method Summary collapse
-
.caught_fault(error, yielder) ⇒ 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.
-
.decode_arguments(payload) ⇒ Object
Decode the Call's payload into its arguments, reporting whether any Capability Handle crossed.
-
.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?.decode_arguments ⇒ [Kobako::Payload::Arguments, bool]
- #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, yielder) ⇒ 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.
The class prefix marks a Service's own exception and nothing else:
it is the <class>: <message> shape a Host App is told to keep
secrets out of, so wearing it says the Service raised. kobako's own
refusals answer under their own wording instead of borrowing that
shape.
The guest's own block failing is not the Service's to report at all, so the Yielder that raised it is asked first — it recognises its own by identity and words the failure the guest's way.
143 144 145 146 147 148 149 150 151 |
# File 'lib/kobako/transport/dispatcher.rb', line 143 def caught_fault(error, yielder) block_failure = yielder&.fault_text(error) return fault("block", block_failure) if block_failure own = OWN_FAULTS.find { |klass, _| error.is_a?(klass) } return fault(own.last, error.) if own fault("runtime", "#{error.class}: #{error.}") end |
.decode_arguments(payload) ⇒ Object
Decode the Call's payload into its arguments, reporting whether any Capability Handle crossed. A codec fault here is a request that never became a call, restated so it cannot read as an unwritable reply — the same restatement #encode_ok makes in the other direction.
108 109 110 111 112 |
# File 'lib/kobako/transport/dispatcher.rb', line 108 def decode_arguments(payload) Kobako::Codec.track_handles { Payload::Arguments.decode(payload) } rescue Kobako::Codec::Error => e raise UnreadableRequestError, "Sandbox could not read the request: #{e.}" 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.
79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/kobako/transport/dispatcher.rb', line 79 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, yielder)] # : [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.
Any other codec fault is the answer failing to encode rather than the request failing to decode, and a Service is the only side that can change what it returns — so it is named here, where the direction is known, instead of falling to the boundary's codec floor and reporting as an exchange that produced no Service outcome.
236 237 238 239 240 241 242 |
# File 'lib/kobako/transport/dispatcher.rb', line 236 def encode_ok(value, handler) Kobako::Codec::Encoder.encode(value) rescue Kobako::Codec::UnsupportedTypeError encode_ok(wrap_as_handle(value, handler), handler) rescue Kobako::Codec::Error => e raise Kobako::SandboxError, "Sandbox could not write the Service's answer: #{e.}" 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.
254 255 256 |
# File 'lib/kobako/transport/dispatcher.rb', line 254 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.
164 165 166 167 168 169 170 171 172 173 |
# File 'lib/kobako/transport/dispatcher.rb', line 164 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.
180 181 182 183 |
# File 'lib/kobako/transport/dispatcher.rb', line 180 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.
218 219 220 221 222 |
# File 'lib/kobako/transport/dispatcher.rb', line 218 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.
189 190 191 192 193 |
# File 'lib/kobako/transport/dispatcher.rb', line 189 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.
121 122 123 124 125 126 |
# File 'lib/kobako/transport/dispatcher.rb', line 121 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
210 211 212 213 214 |
# File 'lib/kobako/transport/dispatcher.rb', line 210 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.
201 202 203 204 205 206 207 208 |
# File 'lib/kobako/transport/dispatcher.rb', line 201 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.
96 97 98 99 100 101 |
# File 'lib/kobako/transport/dispatcher.rb', line 96 def run(call, resolver, handler, yielder) arguments, carried_handle = decode_arguments(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.
247 248 249 |
# File 'lib/kobako/transport/dispatcher.rb', line 247 def wrap_as_handle(value, handler) handler.alloc(value) end |
Instance Method Details
#self?.caught_fault ⇒ [String, String]
25 |
# File 'sig/kobako/transport/dispatcher.rbs', line 25
def self?.caught_fault: (StandardError error, Kobako::Transport::Yielder? yielder) -> [String, String]
|
#self?.decode_arguments ⇒ [Kobako::Payload::Arguments, bool]
23 |
# File 'sig/kobako/transport/dispatcher.rbs', line 23
def self?.decode_arguments: (String payload) -> [Kobako::Payload::Arguments, bool]
|
#self?.dispatch ⇒ [bool, String, String?]
17 |
# File 'sig/kobako/transport/dispatcher.rbs', line 17
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
40 |
# File 'sig/kobako/transport/dispatcher.rbs', line 40
def self?.encode_ok: (untyped value, Kobako::Codec::_HandleTable handler) -> String
|
#self?.fault ⇒ [String, String]
44 |
# File 'sig/kobako/transport/dispatcher.rbs', line 44
def self?.fault: (String type, String message) -> [String, String]
|
#self?.invoke ⇒ Object
27 |
# File 'sig/kobako/transport/dispatcher.rbs', line 27
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.
29 |
# File 'sig/kobako/transport/dispatcher.rbs', line 29
def self?.reject_unreachable!: (untyped target, Symbol name) -> void
|
#self?.require_live_object! ⇒ Object
38 |
# File 'sig/kobako/transport/dispatcher.rbs', line 38
def self?.require_live_object!: (Integer id, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.resolve_arg ⇒ Object
32 |
# File 'sig/kobako/transport/dispatcher.rbs', line 32
def self?.resolve_arg: (untyped value, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.resolve_call_args ⇒ [Array[untyped], Hash[Symbol, untyped]]
21 |
# File 'sig/kobako/transport/dispatcher.rbs', line 21
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
36 |
# File 'sig/kobako/transport/dispatcher.rbs', line 36
def self?.resolve_path: (String path, Kobako::Transport::_ServiceRegistry resolver) -> untyped
|
#self?.resolve_target ⇒ Object
34 |
# File 'sig/kobako/transport/dispatcher.rbs', line 34
def self?.resolve_target: (String | Integer target, Kobako::Transport::_ServiceRegistry resolver, Kobako::Codec::_HandleTable handler) -> untyped
|
#self?.run ⇒ Object
19 |
# File 'sig/kobako/transport/dispatcher.rbs', line 19
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
42 |
# File 'sig/kobako/transport/dispatcher.rbs', line 42
def self?.wrap_as_handle: (untyped value, Kobako::Codec::_HandleTable handler) -> Kobako::Handle
|