Raxol. AgentClientProtocol. Transport. Framer
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
A pure, process-free NDJSON (newline-delimited JSON) line framer.
ACP's stdio transport is a stream of bytes, not a stream of messages: reads arrive in arbitrary chunk sizes (one byte, one line, ten lines, half a line) with no guarantee that chunk boundaries line up with frame boundaries. This module buffers partial data and yields exactly the complete lines a chunk boundary reveals — nothing more, nothing less.
This is intentionally just the byte-splitting layer: it does not
parse JSON (that is Raxol.AgentClientProtocol.Rpc.Message.decode/1's
job) and it is not a process (no GenServer, no mailbox) — a caller
(e.g. the stdio transport) owns reading bytes off the wire and threads
the returned state through successive push/2 calls.
Line splitting
Frames are delimited by \n (LF). A trailing \r immediately before
the \n is trimmed, so CRLF- and LF-terminated peers interoperate
transparently — including within the same stream.
Empty lines (a bare \n, or \r\n with nothing before it) are
skipped rather than yielded as empty frames: some peers send bare
newlines as a keep-alive, and an empty line is never a valid JSON-RPC
frame.
Oversized frames
:max_frame_bytes (default 16777216 = 16 MiB) bounds how much
unterminated data this module will buffer while waiting for the next
\n. A line whose length crosses that bound yields
{:error, {:frame_too_large, size}} in the returned list, in the
position that frame would otherwise have occupied — and then the
framer resyncs: it discards bytes up through the next \n it finds
(which may span several more push/2 calls) and resumes normal
framing from there. One oversized frame never corrupts, drops, or
merges with the frames that follow it. size is the number of bytes
observed at the moment the bound was crossed (for a frame whose
terminator hasn't arrived yet, this is a lower bound on the frame's
true length, not necessarily its final length).
Usage
framer = Framer.new()
{frames, framer} = Framer.push(framer, "{\"a\":1}\n{\"b\":2")
# frames == ["{\"a\":1}"]
{frames, framer} = Framer.push(framer, "}\n")
# frames == ["{\"b\":2}"]
Summary
Types
One complete, decoded-from-bytes frame (still a raw JSON string, not yet parsed).
A frame, or an oversized-frame error occupying that frame's position in the list.
Functions
Create a fresh framer with an empty buffer.
Feed a chunk of bytes (of any size, including empty) into the framer.
Types
@type frame() :: binary()
One complete, decoded-from-bytes frame (still a raw JSON string, not yet parsed).
@type frame_or_error() :: frame() | {:error, {:frame_too_large, non_neg_integer()}}
A frame, or an oversized-frame error occupying that frame's position in the list.
@type t() :: %Raxol.AgentClientProtocol.Transport.Framer{ buffer: binary(), max_frame_bytes: pos_integer(), skipping: boolean() }
Functions
Create a fresh framer with an empty buffer.
Options:
:max_frame_bytes— maximum bytes buffered for a single unterminated line before it is reported as oversized (see moduledoc). Defaults to 16777216 (16 MiB).
@spec push(t(), binary()) :: {[frame_or_error()], t()}
Feed a chunk of bytes (of any size, including empty) into the framer.
Returns {frames, framer}: frames is the (possibly empty) list of
complete frames and/or oversized-frame errors revealed by this chunk,
in wire order; framer is the updated state to pass to the next call.