Module: Kobako::Registry::Dispatcher

Defined in:
lib/kobako/registry/dispatcher.rb

Overview

Pure-function dispatcher for guest-initiated RPC calls. Decodes a msgpack-encoded Request envelope, resolves the target object through the Registry (path lookup or HandleTable lookup), invokes the method, and returns a msgpack-encoded Response envelope.

The module is stateless — all mutable state is threaded through the registry argument so Dispatcher has no instance variables and no side effects beyond mutating the HandleTable via alloc when a non-wire- representable return value must be wrapped (SPEC.md B-14).

Entry point:

Kobako::Registry::Dispatcher.dispatch(request_bytes, registry)
# => msgpack-encoded Response bytes (never raises)

Defined Under Namespace

Classes: DisconnectedTargetError, UndefinedTargetError

Class Method Summary collapse

Class Method Details

.dispatch(request_bytes, registry) ⇒ Object

Dispatch a single RPC request and return the encoded response bytes. Called by Kobako::Registry#dispatch which is invoked from the Rust ext inside __kobako_rpc_call. request_bytes is the msgpack-encoded Request envelope. registry is the live registry for this run, used to resolve path-based targets via #lookup and to access the #handle_table for Handle-based targets and return-value wrapping. Always returns a binary String — never raises. Any failure during decode, lookup, or method invocation is reified as a Response.err envelope so the guest sees the failure as a normal RPC error rather than a wasm trap (SPEC.md B-12).



45
46
47
48
49
50
# File 'lib/kobako/registry/dispatcher.rb', line 45

def dispatch(request_bytes, registry)
  value = perform_dispatch(request_bytes, registry)
  encode_ok_or_wrap(value, registry)
rescue StandardError => e
  encode_dispatch_error(e)
end

.encode_dispatch_error(error) ⇒ Object

Map an error raised during dispatch to a Response.err envelope. error is the StandardError caught at the dispatch boundary. Returns a msgpack-encoded Response envelope (binary). Four error buckets (SPEC.md B-12): Wire::Codec::Error →type=“runtime” (wire decode failed); DisconnectedTargetError →type=“disconnected” (E-14); UndefinedTargetError → type=“undefined” (E-13); ArgumentError → type=“argument” (B-12 arity mismatch); everything else → type=“runtime”.



60
61
62
63
64
65
66
67
68
# File 'lib/kobako/registry/dispatcher.rb', line 60

def encode_dispatch_error(error)
  case error
  when Kobako::Wire::Codec::Error then encode_err("runtime", "wire decode failed: #{error.message}")
  when DisconnectedTargetError then encode_err("disconnected", error.message)
  when UndefinedTargetError    then encode_err("undefined", error.message)
  when ArgumentError           then encode_err("argument", error.message)
  else                              encode_err("runtime", "#{error.class}: #{error.message}")
  end
end

.encode_err(type, message) ⇒ Object



161
162
163
164
165
# File 'lib/kobako/registry/dispatcher.rb', line 161

def encode_err(type, message)
  exception = Kobako::Wire::Exception.new(type: type, message: message)
  response = Kobako::Wire::Envelope::Response.err(exception)
  Kobako::Wire::Envelope.encode_response(response)
end

.encode_ok(value) ⇒ Object



156
157
158
159
# File 'lib/kobako/registry/dispatcher.rb', line 156

def encode_ok(value)
  response = Kobako::Wire::Envelope::Response.ok(value)
  Kobako::Wire::Envelope.encode_response(response)
end

.encode_ok_or_wrap(value, registry) ⇒ Object

Encode value as a Response.ok envelope. When the value is not wire-representable per B-13[link:../../../SPEC.md]‘s type mapping, the UnsupportedType rescue routes it through the HandleTable and re-encodes with the Capability Handle in place (SPEC.md B-14). The happy path encodes exactly once.



150
151
152
153
154
# File 'lib/kobako/registry/dispatcher.rb', line 150

def encode_ok_or_wrap(value, registry)
  encode_ok(value)
rescue Kobako::Wire::Codec::UnsupportedType
  encode_ok(Kobako::Wire::Handle.new(registry.handle_table.alloc(value)))
end

.fetch_live_object(id, handle_table) ⇒ Object

Resolve id through the HandleTable, distinguishing the :disconnected sentinel (E-14) from an unknown id (E-13).



135
136
137
138
139
140
141
142
# File 'lib/kobako/registry/dispatcher.rb', line 135

def fetch_live_object(id, handle_table)
  object = handle_table.fetch(id)
  raise DisconnectedTargetError, "Handle id #{id} is disconnected" if object == :disconnected

  object
rescue Kobako::HandleTableError => e
  raise UndefinedTargetError, e.message
end

.invoke(target, method, args, kwargs) ⇒ Object

Dispatch method on target. kwargs is already Symbol-keyed (the Envelope::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.



84
85
86
87
88
89
90
# File 'lib/kobako/registry/dispatcher.rb', line 84

def invoke(target, method, args, kwargs)
  if kwargs.empty?
    target.public_send(method.to_sym, *args)
  else
    target.public_send(method.to_sym, *args, **kwargs)
  end
end

.perform_dispatch(request_bytes, registry) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/kobako/registry/dispatcher.rb', line 70

def perform_dispatch(request_bytes, registry)
  request = Kobako::Wire::Envelope.decode_request(request_bytes)
  handle_table = registry.handle_table
  target_object = resolve_target(request.target, registry, handle_table)
  args = request.args.map { |v| resolve_arg(v, handle_table) }
  kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handle_table) }
  invoke(target_object, request.method_name, args, kwargs)
end

.resolve_arg(value, handle_table) ⇒ Object

SPEC.md B-16 — A Wire::Handle arriving as a positional or keyword argument identifies a host-side object previously allocated by a prior RPC’s Handle wrap (B-14). Resolve it back to the Ruby object before the dispatch reaches public_send. A Handle whose entry is the :disconnected sentinel (E-14) raises DisconnectedTargetError so the dispatcher emits a Response.err with type=“disconnected”.



98
99
100
101
102
103
104
105
# File 'lib/kobako/registry/dispatcher.rb', line 98

def resolve_arg(value, handle_table)
  case value
  when Kobako::Wire::Handle
    fetch_live_object(value.id, handle_table)
  else
    value
  end
end

.resolve_handle(handle, handle_table) ⇒ Object



129
130
131
# File 'lib/kobako/registry/dispatcher.rb', line 129

def resolve_handle(handle, handle_table)
  fetch_live_object(handle.id, handle_table)
end

.resolve_path(path, registry) ⇒ Object



123
124
125
126
127
# File 'lib/kobako/registry/dispatcher.rb', line 123

def resolve_path(path, registry)
  registry.lookup(path)
rescue KeyError => e
  raise UndefinedTargetError, e.message
end

.resolve_target(target, registry, handle_table) ⇒ Object

Resolve a Request target to the Ruby object the Registry (or HandleTable) holds. String targets go through the Registry; Handle targets (ext 0x01) go through the HandleTable.

Target type is already validated by Wire::Envelope.decode_request before this method is reached, so no else-branch is needed here —the wire layer is the system boundary that enforces the invariant.



114
115
116
117
118
119
120
121
# File 'lib/kobako/registry/dispatcher.rb', line 114

def resolve_target(target, registry, handle_table)
  case target
  when String
    resolve_path(target, registry)
  when Kobako::Wire::Handle
    resolve_handle(target, handle_table)
  end
end