macula_station_client (macula v3.8.0)

View Source

V2 station-client — outbound RPC over macula_peering.

A macula_station_client is a gen_server that owns one macula_peering connection to a single station endpoint. It drives the CONNECT/HELLO handshake as the client side, then sends application-layer CALL frames built with macula_frame:call/1 and matches inbound RESULT/ERROR frames against pending gen_server:call callers using the 16-byte CALL id.

This is the V2 counterpart to macula_mesh_client, which speaks the V1 relay protocol. V1 clients cannot drive V2 stations because V2 stations dispatch the QUIC connection straight into macula_peering:accept/2 — V1 CONNECT frames never reach the V2 handler registry. This module bridges the gap so that a V1-era consumer (e.g. macula-realm's topology subscriber) can issue _dht.find_records_by_type against a V2 station and receive its signed RESULT.

Lifecycle

  1. start_link/1 — spawn worker, schedule connect.
  2. connect_now/1 (cast) — build connect opts, call macula_peering:connect/1, store the worker pid.
  3. Peering handshake completes → {macula_peering, connected, Pid, PeerNodeId} arrives → state moves to connected.
  4. call/4 from caller → build CALL frame, sign happens inside peering, store {from, deadline_timer}` keyed by CALL id, send frame via `macula_peering:send_frame/2.
  5. RESULT or ERROR arrives as {macula_peering, frame, Pid, Frame} → look up call_id, cancel timer, reply to caller.
  6. {macula_peering, disconnected, Pid, Reason} → fail all pending calls with {error, {disconnected, Reason}}, stop the client (caller is responsible for restart / reconnect).

Reply taxonomy

<table><tr><th>Inbound frame</th><th>call/4 returns</th></tr><tr><td>RESULT(payload={error, Reason})</td><td>{ok, {error, Reason}}</td></tr><tr><td>RESULT(payload=Value)</td><td>{ok, Value}</td></tr><tr><td>ERROR(code=C, name=N)</td><td>{error, {call_error, C, N}}</td></tr><tr><td>(deadline elapses)</td><td>{error, timeout}</td></tr><tr><td>(connection drops)</td><td>{error, {disconnected, Reason}}</td></tr></table>

Realm field

V2 CALL frames carry a 32-byte realm id. Stations deployed today advertise an empty realms list (realm-agnostic infrastructure) and do not enforce a realm match on inbound CALLs — the dispatch path verifies the signature and looks up the procedure, nothing more. Callers therefore pass any 32-byte value; this module defaults to all-zeros when no realm is configured.

Summary

Functions

Issue a CALL frame and block until the station replies, the deadline elapses, or the connection drops.

Convenience wrapper for _dht.find_records_by_type. Returns the decoded list of signed records (CBOR-decoded maps as produced by macula_record).

Start a station-client connected to seed. Returns once the gen_server is alive; the QUIC handshake completes asynchronously. Use is_connected/1 to poll readiness or just issue call/4 (which blocks the caller until ready or until its timeout elapses).

Types

opts/0

-type opts() ::
          #{seed := url() | #{host := binary() | string(), port := inet:port_number()},
            identity => macula_identity:key_pair(),
            realm => <<_:256>>,
            capabilities => non_neg_integer(),
            alpn => [binary()],
            connect_timeout_ms => non_neg_integer()}.

url/0

-type url() :: binary() | string().

Functions

call(Pid, Procedure, Payload, TimeoutMs)

-spec call(pid(), binary(), term(), pos_integer()) -> {ok, term()} | {error, term()}.

Issue a CALL frame and block until the station replies, the deadline elapses, or the connection drops.

Procedure is the V2 procedure name, e.g. <<"_dht.find_records_by_type">>. Payload is any term that macula_frame:call/1 accepts (typically a map).

code_change(OldVsn, S, Extra)

find_records_by_type(Pid, Type)

-spec find_records_by_type(pid(), 0..255) -> {ok, [map()]} | {error, term()}.

Convenience wrapper for _dht.find_records_by_type. Returns the decoded list of signed records (CBOR-decoded maps as produced by macula_record).

find_records_by_type(Pid, Type, TimeoutMs)

-spec find_records_by_type(pid(), 0..255, pos_integer()) -> {ok, [map()]} | {error, term()}.

handle_call(Req, From, State)

handle_cast(Msg, S)

handle_info(Other, State)

init(Opts)

is_connected(Pid)

-spec is_connected(pid()) -> boolean().

peer_node_id(Pid)

-spec peer_node_id(pid()) -> {ok, macula_identity:pubkey()} | {error, not_connected}.

start_link(Opts)

-spec start_link(opts()) -> {ok, pid()} | {error, term()}.

Start a station-client connected to seed. Returns once the gen_server is alive; the QUIC handshake completes asynchronously. Use is_connected/1 to poll readiness or just issue call/4 (which blocks the caller until ready or until its timeout elapses).

stop(Pid)

-spec stop(pid()) -> ok.

terminate(Reason, State)