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 catch frame. private_constant is a convention boundary — not a defence.

Returns:

  • (Symbol)
:__kobako_break__

Class Method Summary collapse

Instance Method Summary collapse

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.message}")
  when UndefinedTargetError then fault("undefined", error.message)
  when ArgumentError        then fault("argument", error.message)
  else                           fault("runtime", "#{error.class}: #{error.message}")
  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, message)
  [message.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.message
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.message
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.message
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]

Parameters:

  • error (StandardError)

Returns:

  • ([String, String])


18
# File 'sig/kobako/transport/dispatcher.rbs', line 18

def self?.caught_fault: (StandardError error) -> [String, String]

#self?.dispatch[bool, String, String?]

Parameters:

Returns:

  • ([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_okString

Parameters:

Returns:

  • (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]

Parameters:

  • type (String)
  • message (String)

Returns:

  • ([String, String])


37
# File 'sig/kobako/transport/dispatcher.rbs', line 37

def self?.fault: (String type, String message) -> [String, String]

#self?.invokeObject

Parameters:

Returns:

  • (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.

Parameters:

  • target (Object)
  • name (Symbol)


22
# File 'sig/kobako/transport/dispatcher.rbs', line 22

def self?.reject_unreachable!: (untyped target, Symbol name) -> void

#self?.require_live_object!Object

Parameters:

Returns:

  • (Object)


31
# File 'sig/kobako/transport/dispatcher.rbs', line 31

def self?.require_live_object!: (Integer id, Kobako::Codec::_HandleTable handler) -> untyped

#self?.resolve_argObject

Parameters:

Returns:

  • (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]]

Parameters:

Returns:

  • ([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_pathObject

Parameters:

Returns:

  • (Object)


29
# File 'sig/kobako/transport/dispatcher.rbs', line 29

def self?.resolve_path: (String path, Kobako::Transport::_ServiceRegistry resolver) -> untyped

#self?.resolve_targetObject

Parameters:

Returns:

  • (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?.runObject

Parameters:

Returns:

  • (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_handleKobako::Handle

Parameters:

Returns:



35
# File 'sig/kobako/transport/dispatcher.rbs', line 35

def self?.wrap_as_handle: (untyped value, Kobako::Codec::_HandleTable handler) -> Kobako::Handle