# `Raxol.AgentClientProtocol.Transport.Framer`
[🔗](https://github.com/DROOdotFOO/raxol-agent/blob/raxol_agent_client_protocol-v0.1.0/packages/raxol_agent_client_protocol/lib/raxol/agent_client_protocol/transport/framer.ex#L1)

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}"]

# `frame`

```elixir
@type frame() :: binary()
```

One complete, decoded-from-bytes frame (still a raw JSON string, not yet parsed).

# `frame_or_error`

```elixir
@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.

# `t`

```elixir
@type t() :: %Raxol.AgentClientProtocol.Transport.Framer{
  buffer: binary(),
  max_frame_bytes: pos_integer(),
  skipping: boolean()
}
```

# `new`

```elixir
@spec new(keyword()) :: t()
```

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).

# `push`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
