Every Ircxd.Client uses an Ircxd.Client.Transport adapter. The default
Ircxd.Client.Transport.Socket adapter contains Ircxd's existing TCP/TLS connection behavior, so
existing callers need no code or configuration changes. Ircxd.Client.Info.transport continues to
report :gen_tcp or :ssl for the default adapter.
Set :transport_adapter only when another process or service must own the connection while Ircxd
continues to own IRC parsing, protocol state, validation, commands, and events:
Ircxd.Client.start_link(
host: "irc.example.net",
port: 6697,
tls: true,
nick: "example",
transport_adapter: {MyApp.IrcTransport, connection_id: 42}
)The adapter receives the client PID and credential-free endpoint settings in connect/3. Raw TLS
options remain private to Ircxd, and retained checkpoints contain no credentials. However,
send_data/2 receives complete IRC wire records and therefore sees PASS, WEBIRC, SASL payloads,
and any later command containing sensitive data. Adapters must protect that data in transit and
must not log wire records. The default socket adapter receives TLS options through its private
initialization argument.
Adapter contract
An adapter implements Ircxd.Client.Transport:
defmodule MyApp.IrcTransport do
@behaviour Ircxd.Client.Transport
@impl true
def connect(client, config, opts) do
case MyConnectionOwner.attach(client, config, opts) do
{:ok, handle} -> {:ok, handle, :fresh}
{:ok, handle, checkpoint, metadata} ->
{:ok, handle, {:resumed, checkpoint, metadata}}
{:error, reason} -> {:error, reason}
end
end
@impl true
def send_data(handle, data), do: MyConnectionOwner.send_data(handle, data)
# Optional. Implement this only when the owner can suppress a repeated logical write.
@impl true
def send_data_once(handle, keys, data) do
MyConnectionOwner.send_data_once(handle, keys, data)
end
@impl true
def activate(_handle), do: :ok
@impl true
def checkpoint?(_handle), do: true
@impl true
def accepted(handle, receipt, checkpoint) do
# This runs in the Ircxd client process after its events have been sent. Relaying a marker to
# the same event consumer preserves mailbox order with those events.
send(MyConnectionOwner.consumer(handle), {:irc_record_accepted, handle, receipt, checkpoint})
:ok
end
@impl true
def handle_info(_message, _handle), do: :unknown
@impl true
def close(handle, reason), do: MyConnectionOwner.detach(handle, reason)
endThe handle is adapter-owned and must identify one connection attempt. Ircxd ignores data and close
events carrying an earlier handle after reconnect. close/2 must tolerate repeated calls because
the upstream owner and Ircxd can observe closure concurrently. A cleanup error prevents reconnect;
Ircxd will not open a second handle while the adapter reports that the prior one may remain active.
activate/1 enables the next inbound record. It is a no-op for an owner that already implements
its own flow control. handle_info/2 lets an adapter translate process messages it owns; return
:unknown for unrelated messages. The default socket adapter uses both callbacks for active-once
TCP/TLS reads.
Set checkpoint?/1 to false when the transport cannot resume. This avoids constructing a parser
checkpoint for each record. A resumable transport returns true.
Optional idempotent writes
send_data_once/3 is an optional callback for a connection owner that can remember logical writes
for the lifetime of its handle. Ircxd.Client.join/3 and Ircxd.Client.transmit/3 accept an
:idempotency_keys option. Ircxd requires a non-empty list of non-empty binaries, removes duplicate
keys while preserving their first-seen order, serializes the IRC record once, and passes the keys
and record to send_data_once/3.
The adapter owns key retention and overlap policy; Ircxd does not place outbound keys in its parser checkpoint. A multi-key write should be atomic from the adapter's perspective: write when none of the keys were recorded, suppress the retry when all were recorded, and return an error on partial overlap instead of guessing that part of the IRC record was sent. Record keys only after the underlying write succeeds. If the outcome is uncertain, return an error and do not follow it with an ordinary write for the same attempt.
If an adapter does not implement send_data_once/3, Ircxd deliberately falls back to
send_data/2. This keeps existing adapters and the default TCP/TLS socket transport compatible, but
the fallback provides no duplicate suppression. Callers that require the guarantee must select an
adapter that implements the optional callback and defines a bounded key-retention policy.
Inbound records and acceptance
The adapter or its connection owner delivers one complete IRC record with:
Ircxd.Client.Transport.deliver(client, handle, receipt, line)Ircxd parses the line, sends all resulting events, calls accepted/3 with the post-record parser
checkpoint, and then activates the next read. Parse errors also reach accepted/3; a malformed
record is terminally consumed rather than replayed forever.
accepted/3 means the Ircxd parser accepted the record. It does not by itself mean a separate host
application durably processed the resulting events. A durable transport should use accepted/3 to
send an ordered marker to the same process receiving Ircxd events. That consumer first handles the
preceding events and then atomically stores checkpoint while acknowledging receipt upstream.
Do not acknowledge the durable record merely because it was delivered to the Ircxd process.
Before IRC registration completes, checkpoint is nil. If the client or host crashes in that
state, the owner must discard the retained connection and establish a fresh one. Ircxd instead
passes {:unavailable, reason} when registered parser state cannot be safely retained, including an
oversized checkpoint or deferred server-time events. The owner must not acknowledge that record;
close the retained generation and establish a fresh connection.
Report upstream closure with:
Ircxd.Client.Transport.closed(client, handle, reason)The existing :reconnect option remains authoritative. A reconnect calls the selected adapter's
connect/3 again.
Fresh connections
Return {:ok, handle, :fresh} for a new, unregistered upstream connection. Ircxd sends its normal
WEBIRC, PASS, CAP, NICK, USER, and optional SASL registration traffic through send_data/2.
Resumed connections
Return {:ok, handle, {:resumed, checkpoint, metadata}} only for an upstream connection that is
already registered. Ircxd restores the checkpoint and does not send registration traffic.
The checkpoint is opaque, versioned, integrity-checked, and limited to 65,536 bytes in its
uncompressed Erlang external-term envelope. Retain the exact value received by accepted/3; do not
construct or edit it. It contains bounded inbound parser state needed across a restart, including
current nick, negotiated capabilities, ISUPPORT, message-ID deduplication, and in-progress batch,
multiline, metadata, labeled-response, and network-batch state.
Outbound request tracking and generated multiline references remain process-local. Outbound command parameters can contain credentials, so Ircxd never places them in a checkpoint. Server passwords, SASL and WebIRC credentials, raw TLS options, callbacks, PIDs, and timer references are also excluded. Deferred server-time buffers and parser state larger than the checkpoint envelope make the current connection non-resumable instead of creating a partial checkpoint.
Checkpoints are bound to the connection and parser configuration that created them, including host, port, TLS mode, SNI, non-secret TLS verification policy, desired nick, username, real name, requested capabilities, public SASL and WebIRC identity, message-ID mode, and server-time ordering. Ircxd rejects an unknown version, changed integrity digest, malformed payload, or mismatched binding.
Set the non-secret :resume_binding option to a deployment or credential generation token, and
rotate it when server, SASL, or WebIRC secrets change or when TLS material changes without changing
its configured path. The token itself is hashed into the binding. Ircxd automatically fingerprints
inline CA/certificate material and hostname-verification policy, but deliberately does not retain
private keys or passwords in any form.
After restoration, the client publishes these events in order:
{:connected, connection_metadata}
{:resumed, transport_metadata}
:registeredThe adapter-defined metadata is not protocol state. It may identify the retained connection generation, replay count, or detached duration.
A replay gap is not safely repairable from an IRC client cache: the missed records could include nick, capability, ISUPPORT, membership, or batch state changes. If the owner reports a gap, if no valid registered checkpoint exists, or if Ircxd rejects the checkpoint, close that generation and establish a fresh IRC connection. Never send registration commands through an already registered upstream socket.