asobi_rpc (asobi v0.72.5)
View SourceThe extension RPC dispatcher: rpc.call in, rpc.ok or rpc.error out.
rpc/0 in an extension's manifest declares Method => {M, F, A}. This is
what reads it. Without this module the declaration is validated and then
nothing calls it, which is the state the first real extension reported.
The frame
{"type": "rpc.call", "cid": "c-1",
"payload": {"protocol": 1, "method": "quests.claim", "params": {"quest_id": "q-1"}}}{"type": "rpc.ok", "cid": "c-1", "payload": {"result": {"reward": 100}}}
{"type": "rpc.error", "cid": "c-1", "payload": {"error": {"code": "...", "message": "...", "details": {}}}}cidis required and validated here, unlike the rest of the socket where it is an optional echo. An RPC reply is useless without correlation - it is the only way a client pairsrpc.okwith the call it made - and the value is echoed into a frame, so it is bounded to 1-64 printable ASCII bytes rather than reflected unchecked. A rejectedcidis not echoed back; there is nothing trustworthy to echo.paramsandresultare always objects. A bare scalar cannot grow a field later without breaking every client, and this is the one wire shape seven SDKs must agree on.protocolis a number, currently1. Version the payload, not the frame type, so a future version is a rejection a client can read rather than anunknown_type.
The handler contract
-spec claim(asobi_rpc:params(), asobi_rpc:ctx()) -> asobi_rpc:reply().
claim(#{~"quest_id" := QuestId}, #{player_id := PlayerId}) ->
case asobi_quests:claim(PlayerId, QuestId) of
{ok, Reward} -> {ok, #{reward => Reward}};
{error, already_done} -> {error, ~"quests.already_claimed"}
end.(Params, Ctx) -> {ok, map()} | {error, Code} | {error, Code, Details}, and
the arity in rpc/0 is therefore always 2.
The failure half is a code, not a status and an object. Both of those are
derivable from the code - asobi_error:status/1 and asobi_error:object/2 -
and an extension mints its own codes in its own domain (codes/0), so
returning a status would let two call sites answer the same code differently
and would put object construction in every extension. It is also the dialect
core's own controllers already speak ({asobi_error, Code, Details}), so
there is one shape to learn rather than two.
Ctx is #{player_id, session, method} and may gain keys. Match the ones
you need with :=; never match it exhaustively.
An extension error surfaces as its own code, provided the extension
declared it in codes/0. A handler that raises, returns outside the contract,
or returns a code nobody declared becomes internal, each logged as the
extension defect it is. The last of those is what keeps the code set closed on
this surface: every other call site's code is a binary literal checked at build
time, and a handler's is a runtime term that could have been built from
params.
Readiness
The route table compiles inside nova_sup:init/1; migrations run afterwards
from asobi_app:start/2. A socket is therefore reachable before an
extension's tables exist, so every dispatch passes asobi_readiness:guard/0
first and answers not_ready (503) until migrations have finished.
Authorisation
Every declared method is player-scoped: the caller is the authenticated
player on that socket, and an unauthenticated socket gets unauthenticated
before anything is looked up. There is deliberately no per-method capability
class. read | player_data | config (ADR 0007) is an operator
vocabulary, minted by asobi_ops_auth for the ops plane and never held by
a player, so tagging a socket method with one would make it deniable for
every caller this dispatcher has - another declaration nothing can reach.
The reachable home for an operator-only extension method is the ops plane,
which is read-only today by assertion, so opening it is a decision about that
plane rather than about this manifest.
Summary
Types
The authenticated player behind the socket, or unauthenticated.
What a handler is told about its caller. Additive; never match it exhaustively.
The params object, exactly as the client sent it. Keys are binaries.
What a handler returns.
Types
The authenticated player behind the socket, or unauthenticated.
What a handler is told about its caller. Additive; never match it exhaustively.
The params object, exactly as the client sent it. Keys are binaries.
-type reply() :: {ok, map()} | {error, asobi_error:code()} | {error, asobi_error:code(), asobi_error:details()}.
What a handler returns.
Functions
-spec handle(term(), term(), caller()) -> {binary() | undefined, {ok, map()} | {error, asobi_error:object()}}.
Dispatch one rpc.call.
Returns the cid to echo - undefined when the client's own cid was
rejected - and the outcome to encode.
-spec protocol() -> pos_integer().
The RPC protocol version this node speaks.