Raxol. AgentClientProtocol. Transport. Paired
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
An in-process, zero-copy pair of Raxol.AgentClientProtocol.Transport
handles. This is the test backbone for the rest of the library and the
BEAM-local wiring for an agent/client pair that live in the same VM.
create_pair/0 starts two lightweight GenServers — one per side — and
links them to each other as peers. A message sent on the left handle is
delivered to the right handle's owner (and vice versa), preserving send
order. No JSON encoding happens anywhere in this module: Elixir maps
pass straight through by reference. Byte-level codec correctness
(encoding, framing, partial reads) is exercised by the stdio transport
instead, not here.
Ownership
A freshly created handle has no owner (nil), so a supervisor can
create the pair before the Connection process that will adopt it
exists. Whatever arrives before an owner is set — peer frames and a
peer close — is held, and set_owner/2 delivers it to the new owner in
arrival order before anything later: the other side may start sending
(e.g. a client's initialize) before this side's Connection has
adopted its handle. Call set_owner/2 to adopt (or re-adopt — handoff
to a new owner is supported at any time, including mid-stream).
Ordering
Erlang guarantees that messages sent between any two given processes
arrive in send order. send_message/2 accepts a frame locally (a
single, non-nested GenServer.call/2) and hands it off to the peer's
side with GenServer.cast/2. Because a side handles its own incoming
:send calls one at a time, in arrival order, the casts it issues to
the peer land on the peer's mailbox in that same order — so a single
sending process sees strict order preserved end to end, with no
explicit sequence number needed.
This deliberately avoids a synchronous call into the peer from
inside a side's own handle_call/3: two sides calling each other
synchronously at the same time is a classic mutual-deadlock — each
side blocks inside its callback waiting on the other, and neither can
drain its mailbox to answer. Keeping the peer hop async (cast) is
what makes simultaneous bidirectional traffic (both ends sending at
once) safe.
Closing
close/1 is idempotent and marks the local side closed immediately.
It also notifies the peer (a cast), which marks itself closed and
delivers {:closed, :peer_closed} to its own owner — the side that
calls close/1 does not message its own owner. Once a side's closed
flag is set (locally, by close/1, or asynchronously via the peer
notification), further send_message/2 calls through it return
{:error, :closed}, and any :deliver that arrives after closing is
dropped rather than reaching the owner. The underlying processes are
not killed by close/1; a handle remains a valid (but permanently
closed) tuple that can still be inspected.
Summary
Functions
Returns a specification to start this module under a supervisor.
Create a connected pair of transport handles: {left, right}.
Set (or replace) the owner process for a handle. The owner is the
process that will receive {:acp_transport, transport_ref, ...}
messages for this handle going forward, starting with any held since
the handle was created.
Types
@type t() :: %Raxol.AgentClientProtocol.Transport.Paired{pid: pid()}
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Create a connected pair of transport handles: {left, right}.
Neither handle has an owner yet; inbound traffic is held until
set_owner/2 adopts the handle.
Set (or replace) the owner process for a handle. The owner is the
process that will receive {:acp_transport, transport_ref, ...}
messages for this handle going forward, starting with any held since
the handle was created.