Module: Kobako::Transport::Dispatcher
- Defined in:
- lib/kobako/transport/dispatcher.rb
Overview
Pure-function dispatcher for guest-initiated transport calls. Decodes a msgpack-encoded Request envelope, resolves the target object through the Catalog::Namespaces (path lookup) or Catalog::Handles (Handle lookup), invokes the method, and returns a msgpack-encoded Response envelope.
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.
Entry point:
Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
# => msgpack-encoded Response bytes (never raises)
Defined Under Namespace
Classes: UndefinedTargetError
Class Method Summary collapse
-
.dispatch(request_bytes, namespaces, handler, yield_to_guest) ⇒ Object
Dispatch a single transport request and return the encoded Response bytes.
-
.encode_caught_error(error) ⇒ Object
Map an error caught at the dispatch boundary to a
Response.errorenvelope. - .encode_error(type, message) ⇒ Object
-
.encode_ok(value, handler) ⇒ Object
Encode
valueas aResponse.okenvelope. -
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch
methodontarget. -
.reject_meta_method!(target, name) ⇒ Object
Guard the
public_sendbelow against ambient reflection methods. -
.reject_unexposed!(target, name) ⇒ Object
Consult the target’s opt-in narrowing predicate.
-
.require_live_object!(id, handler) ⇒ Object
Resolve
idthrough the Catalog::Handles. -
.resolve_arg(value, handler) ⇒ Object
A Kobako::Handle arriving as a positional or keyword argument identifies a host-side object previously allocated by a prior transport call’s Handle wrap.
-
.resolve_call_args(request, handler) ⇒ Object
Resolve positional and keyword arguments off
requestin one step. - .resolve_handle(handle, handler) ⇒ Object
- .resolve_path(path, namespaces) ⇒ Object
-
.resolve_target(target, namespaces, handler) ⇒ Object
Resolve a Request target to the Ruby object the registry (or Catalog::Handles) holds.
-
.wrap_as_handle(value, handler) ⇒ Object
Allocate
valuein the Sandbox’s Catalog::Handles and return aHandlethat the wire codec can carry.
Class Method Details
.dispatch(request_bytes, namespaces, handler, yield_to_guest) ⇒ Object
Dispatch a single transport request and return the encoded Response bytes. Invoked from the Runtime#on_dispatch Proc that Kobako::Sandbox#initialize installs on the ext side; namespaces, handler, and yield_to_guest are captured in that Proc’s closure so the Dispatcher stays stateless and the registry doesn’t need to publish accessors for the Sandbox-owned Catalog::Handles or Runtime. yield_to_guest is a String → String callable (typically Runtime#yield_to_active_invocation bound as a lambda) used only when the Request carries block_given: true. Always returns a binary String — every failure path is reified as a Response.error envelope so the guest sees a transport error rather than a wasm trap.
80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/kobako/transport/dispatcher.rb', line 80 def dispatch(request_bytes, namespaces, handler, yield_to_guest) request = Kobako::Transport::Request.decode(request_bytes) target = resolve_target(request.target, namespaces, handler) args, kwargs = resolve_call_args(request, handler) yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) } encode_ok(value, handler) rescue StandardError => e encode_caught_error(e) ensure yielder&.invalidate! end |
.encode_caught_error(error) ⇒ Object
Map an error caught at the dispatch boundary to a Response.error envelope. error is the StandardError caught by #dispatch‘s rescue. Returns a msgpack-encoded Response envelope (binary). Four error buckets: Kobako::Codec::Error → type=“runtime” (malformed request); UndefinedTargetError → type=“undefined”; ArgumentError →type=“argument” (arity mismatch); everything else →type=“runtime”.
110 111 112 113 114 115 116 117 118 |
# File 'lib/kobako/transport/dispatcher.rb', line 110 def encode_caught_error(error) case error when Kobako::Codec::Error then encode_error("runtime", "Sandbox received a malformed request: #{error.}") when UndefinedTargetError then encode_error("undefined", error.) when ArgumentError then encode_error("argument", error.) else encode_error("runtime", "#{error.class}: #{error.}") end end |
.encode_error(type, message) ⇒ Object
238 239 240 241 242 |
# File 'lib/kobako/transport/dispatcher.rb', line 238 def encode_error(type, ) fault = Kobako::Fault.new(type: type, message: ) response = Kobako::Transport::Response.error(fault) response.encode end |
.encode_ok(value, handler) ⇒ Object
Encode value as a Response.ok envelope. When the value is not wire-representable per the codec’s type mapping, the UnsupportedType 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.
224 225 226 227 228 229 |
# File 'lib/kobako/transport/dispatcher.rb', line 224 def encode_ok(value, handler) response = Kobako::Transport::Response.ok(value) response.encode rescue Kobako::Codec::UnsupportedType encode_ok(wrap_as_handle(value, handler), handler) end |
.invoke(target, method, args, kwargs, yielder = nil) ⇒ Object
Dispatch method on target. kwargs is already Symbol-keyed (the Request 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.
131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/kobako/transport/dispatcher.rb', line 131 def invoke(target, method, args, kwargs, yielder = nil) name = method.to_sym (target, name) reject_unexposed!(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_meta_method!(target, name) ⇒ Object
Guard the public_send below against ambient reflection methods. A public method whose owner is a META_OWNERS or GADGET_OWNERS module is rejected, except CALLABLE_ALLOW on a gadget target (a bound lambda stays invocable). A name with no concrete public method is allowed only when the target opts into it via respond_to? (dynamic method_missing Services), since the dangerous methods are all concretely defined and therefore never reach that branch.
150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/kobako/transport/dispatcher.rb', line 150 def (target, name) owner = target.public_method(name).owner gadget = GADGET_OWNERS.include?(owner) return unless META_OWNERS.include?(owner) || gadget return if gadget && CALLABLE_ALLOW.include?(name) raise UndefinedTargetError, "method #{name.inspect} is not a Service method" rescue NameError return if target.respond_to?(name) raise UndefinedTargetError, "no public method #{name.inspect} on target" end |
.reject_unexposed!(target, name) ⇒ Object
Consult the target’s opt-in narrowing predicate. A bound object may define a private respond_to_guest?(name) to restrict which of its methods the guest reaches; a falsy answer rejects the dispatch. The predicate composes beneath #reject_meta_method! — it only narrows, never re-opening the reflection surface the floor rejects — and is consulted with the private surface included so the guest’s public_send dispatch can never reach respond_to_guest? itself.
170 171 172 173 174 175 |
# File 'lib/kobako/transport/dispatcher.rb', line 170 def reject_unexposed!(target, name) return unless target.respond_to?(:respond_to_guest?, true) return if target.__send__(:respond_to_guest?, name) raise UndefinedTargetError, "method #{name.inspect} is not exposed to the guest" end |
.require_live_object!(id, handler) ⇒ Object
Resolve id through the Catalog::Handles. An unknown id surfaces as UndefinedTargetError.
213 214 215 216 217 |
# File 'lib/kobako/transport/dispatcher.rb', line 213 def require_live_object!(id, handler) handler.fetch(id) rescue Kobako::SandboxError => e raise UndefinedTargetError, e. end |
.resolve_arg(value, handler) ⇒ Object
A Kobako::Handle arriving as a positional or keyword argument identifies a host-side object previously allocated by a prior transport call’s Handle wrap. Resolve it back to the Ruby object before the dispatch reaches public_send.
181 182 183 |
# File 'lib/kobako/transport/dispatcher.rb', line 181 def resolve_arg(value, handler) value.is_a?(Kobako::Handle) ? require_live_object!(value.id, handler) : value end |
.resolve_call_args(request, handler) ⇒ Object
Resolve positional and keyword arguments off request in one step. Both pass through #resolve_arg so Capability Handles round-trip back to the host-side Ruby object before the call reaches public_send.
97 98 99 100 |
# File 'lib/kobako/transport/dispatcher.rb', line 97 def resolve_call_args(request, handler) [request.args.map { |v| resolve_arg(v, handler) }, request.kwargs.transform_values { |v| resolve_arg(v, handler) }] end |
.resolve_handle(handle, handler) ⇒ Object
207 208 209 |
# File 'lib/kobako/transport/dispatcher.rb', line 207 def resolve_handle(handle, handler) require_live_object!(handle.id, handler) end |
.resolve_path(path, namespaces) ⇒ Object
201 202 203 204 205 |
# File 'lib/kobako/transport/dispatcher.rb', line 201 def resolve_path(path, namespaces) namespaces.lookup(path) rescue KeyError => e raise UndefinedTargetError, e. end |
.resolve_target(target, namespaces, handler) ⇒ Object
Resolve a Request target to the Ruby object the registry (or Catalog::Handles) holds. String targets go through the registry; Handle targets (ext 0x01) go through the Catalog::Handles.
Target type is already validated by Transport::Request.decode before this method is reached, so no else-branch is needed here —the wire layer is the system boundary that enforces the invariant.
192 193 194 195 196 197 198 199 |
# File 'lib/kobako/transport/dispatcher.rb', line 192 def resolve_target(target, namespaces, handler) case target when String resolve_path(target, namespaces) when Kobako::Handle resolve_handle(target, handler) end 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.
234 235 236 |
# File 'lib/kobako/transport/dispatcher.rb', line 234 def wrap_as_handle(value, handler) handler.alloc(value) end |