Hermolaos.Transport.MessageBuffer (Hermolaos v0.3.0)

View Source

Buffer for handling newline-delimited JSON messages.

MCP stdio transport uses newline-delimited JSON, where each JSON-RPC message is on its own line. This buffer handles:

  • Accumulating partial data chunks
  • Splitting on newlines to extract complete messages
  • Decoding JSON messages
  • Preserving incomplete data for the next chunk

Design Notes

This module is designed for high performance:

  • Uses binary operations for efficient string handling
  • Minimizes copying by using binary references where possible
  • Handles edge cases like empty lines and partial JSON

Examples

iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> {messages, buffer} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s({"id":1}\n))
iex> messages
[%{"id" => 1}]

# Partial messages are buffered
iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> {[], buffer} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s({"id":))
iex> {[%{"id" => 1}], _} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s(1}\n))

Summary

Functions

Appends data to the buffer and extracts any complete messages.

Returns the current buffer size in bytes.

Checks if the buffer has pending (incomplete) data.

Creates a new empty message buffer.

Resets the buffer, discarding any buffered data.

Returns buffer statistics.

Types

stats()

@type stats() :: %{
  messages_received: non_neg_integer(),
  bytes_received: non_neg_integer(),
  parse_errors: non_neg_integer()
}

t()

@type t() :: %Hermolaos.Transport.MessageBuffer{buffer: binary(), stats: stats()}

Functions

append(state, data)

@spec append(t(), binary()) :: {[map()], t()}

Appends data to the buffer and extracts any complete messages.

Returns a tuple of {messages, new_buffer} where:

  • messages is a list of decoded JSON maps (may be empty)
  • new_buffer is the updated buffer with any incomplete data preserved

Parameters

  • buffer - The current buffer state
  • data - Binary data to append

Examples

# Single complete message
iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> {msgs, _} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s({"jsonrpc":"2.0"}\n))
iex> msgs
[%{"jsonrpc" => "2.0"}]

# Multiple messages in one chunk
iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> {msgs, _} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s({"id":1}\n{"id":2}\n))
iex> length(msgs)
2

# Partial message preserved
iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> {[], buffer} = Hermolaos.Transport.MessageBuffer.append(buffer, ~s({"partial":))
iex> buffer.buffer
~s({"partial":)

buffer_size(message_buffer)

@spec buffer_size(t()) :: non_neg_integer()

Returns the current buffer size in bytes.

Examples

iex> buffer = %Hermolaos.Transport.MessageBuffer{buffer: "12345"}
iex> Hermolaos.Transport.MessageBuffer.buffer_size(buffer)
5

has_pending?(message_buffer)

@spec has_pending?(t()) :: boolean()

Checks if the buffer has pending (incomplete) data.

Examples

iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> Hermolaos.Transport.MessageBuffer.has_pending?(buffer)
false

iex> buffer = %Hermolaos.Transport.MessageBuffer{buffer: "pending"}
iex> Hermolaos.Transport.MessageBuffer.has_pending?(buffer)
true

new()

@spec new() :: t()

Creates a new empty message buffer.

Examples

iex> Hermolaos.Transport.MessageBuffer.new()
%Hermolaos.Transport.MessageBuffer{buffer: "", stats: %{messages_received: 0, bytes_received: 0, parse_errors: 0}}

reset(state)

@spec reset(t()) :: {[map()], t()}

Resets the buffer, discarding any buffered data.

Returns any messages that could be extracted from the remaining buffer before clearing it.

Examples

iex> buffer = %Hermolaos.Transport.MessageBuffer{buffer: "partial data"}
iex> {[], new_buffer} = Hermolaos.Transport.MessageBuffer.reset(buffer)
iex> new_buffer.buffer
""

stats(message_buffer)

@spec stats(t()) :: stats()

Returns buffer statistics.

Examples

iex> buffer = Hermolaos.Transport.MessageBuffer.new()
iex> Hermolaos.Transport.MessageBuffer.stats(buffer)
%{messages_received: 0, bytes_received: 0, parse_errors: 0}