actions/rpc_client.ts

Typed RPC client — creates a Proxy-based API from action specs.

Two tiers of usage: - Tier 1 (simple, for tx): transport send/receive, Result return. No environment. - Tier 2 (full, for zzz): ActionEvent lifecycle with environment.

Pass the consumer's generated ActionsApi interface as <TApi> to flow full type safety through without an explicit cast at the call site.

Declarations
#

6 declarations

view source

create_rpc_client
#

actions/rpc_client.ts view source

<TApi extends object>(options: CreateRpcClientOptions<TApi>): TApi

Creates a Proxy-based API from action specs.

Method calls are dynamically dispatched based on the action spec's kind: - request_response → send request, await response, return Result - remote_notification → send notification, return Result - local_call → execute locally (sync or async), return Result or throw

Generic TApi is the consumer's typed Proxy interface (typically a codegen-derived ActionsApi). Required — no default, so forgetting it is a type error rather than a silent slide into any. The `as unknown as TApi` coercion lives inside this function so call sites get a typed return without a cast at the seam. TApi is a type-layer promise about what the Proxy responds to; the runtime walks specs (kept in sync by the consumer, codegen recommended).

const api_result = create_rpc_client<MyActionsApi>({peer, environment});

options

type CreateRpcClientOptions<TApi>

returns

TApi

a Proxy typed as TApi that responds to any method name found in the environment's specs

create_throwing_api
#

actions/rpc_client.ts view source

<TApi extends object>(api_result: TApi): ThrowingApi<TApi>

Wrap a typed RPC client so every call resolves to its unwrapped value or throws an Error carrying the JSON-RPC {code, message, data?} shape.

Implementation is a Proxy because the underlying create_rpc_client return is itself a Proxy with no concrete keys — a key-by-key wrap would need to enumerate the typed surface, which only the consumer's generated ActionsApi interface knows.

Pass-through on non-Result returns is deliberate: sync local_call Proxy methods return values directly (see create_sync_local_call_method above). The Proxy can't distinguish those at get-time, so the wrapper inspects result shape at call-time and only unwraps when it sees a Result. Non-object returns pass through unchanged.

Only {code, data} cross onto the thrown Error — name / stack are left as the Error's own properties so attacker-shaped result.error payloads cannot overwrite them.

Recommended consumer convention: create_frontend_rpc_client ships both shapes by default — api (throwing) for hot-path call sites and api_result (Result) for sites that inspect error.data.reason without try/catch. Result is the protocol primitive; this wrapper is the ergonomic layer over it. Picking is per call site — both Proxies share the same underlying transport.

Catch blocks read err.data?.reason — optional chaining required because JSON-RPC data is spec-level optional.

On unknown string-keyed methods, the get trap returns a function that throws "rpc method not found: <prop>" on invocation — clearer than the JS default "api.foo is not a function". Symbol props and then stay undefined so the Proxy isn't accidentally treated as a thenable (await api would otherwise probe then and trip the thrower).

api_result

typed Result-returning RPC client from create_rpc_client<ActionsApi>(...). The "_result" suffix names what the underlying calls return (Result<{value}, {error}>).

type TApi

returns

ThrowingApi<TApi>

CreateRpcClientOptions
#

actions/rpc_client.ts view source

CreateRpcClientOptions<TApi>

Options for create_rpc_client.

generics

TApi

constraint object
default object

peer

environment

on_action_event

Optional callback fired once per dispatched action with the live ActionEvent. Consumers wire reactive state here — e.g. zzz's Actions cell calls add_from_json + listen_to_action_event inside the callback so its history stays decoupled from the rpc_client surface.

event.spec.method and event.data.method narrow to keyof TApi & string — drop the as ActionMethod cast at the call site when TApi is a generated ActionsApi interface.

type (event: ActionEvent<keyof TApi & string>) => void

transport_for_method

Optional per-method transport selector. When provided, the client calls peer.send(msg, {transport_name}) with the returned transport for each request_response / remote_notification dispatch. Returning undefined falls back to the peer's default selection.

RpcClientCallOptions
#

ThrowingApi
#

actions/rpc_client.ts view source

ThrowingApi<TApi>

Maps a typed ActionsApi to a throwing variant.

For each method whose return type matches the create_rpc_client shape (Promise<Result<{value: T}, {error: JsonrpcErrorObject}>>), the wrapped method returns Promise<T> directly. Other shapes (notifications typed as => void, sync local_call methods) pass through unchanged — there is nothing to unwrap.

Input + options parameters are preserved verbatim via ...args: infer TArgs so the conditional matches both required-input (input: T) and optional-input (input?: T / nullary) signatures uniformly. Required-input shapes (e.g. admin_session_revoke_all(input: AdminSessionRevokeAllInput)) are not assignable to a (input?: TInput) => … pattern under --strictFunctionTypes, so an earlier (input?, options?) => form silently fell through to TApi[K] and left those methods Result-shaped — create_admin_rpc_adapters(api) would then reject the typed throwing Proxy because half its surface still returned Result<...>. The rest-args form preserves both required and optional parameters and resolves the gap.

generics

TApi

TransportForMethod
#

actions/rpc_client.ts view source

TransportForMethod

Optional per-method transport selector. Return the transport to use for a given method, or undefined to let the peer pick via its fallback rules.

Useful when methods are registered on different backend dispatchers — e.g. a streaming action mounted on the WebSocket endpoint while the rest of the RPC surface lives on HTTP.

Depends on
#

Imported by
#