WebsockexNova.Behaviors.MessageHandler behaviour (WebsockexNova v0.1.0)
View SourceDefines the behavior for handling WebSocket messages.
The MessageHandler behavior is part of WebsockexNova's thin adapter architecture, allowing client applications to customize message processing while maintaining a clean separation from transport concerns.
Binary Data Support
WebsockexNova supports both structured map data and raw binary data throughout the message handling pipeline:
handle_message/2
can receive and process both maps and binary dataencode_message/2
can return either maps or binary payloads- Binary data is preserved in its original form when appropriate
- Implementations can choose how to handle different data formats
This flexibility allows WebsockexNova to work with any WebSocket protocol, whether it uses structured data like JSON or raw binary formats.
Thin Adapter Pattern
As part of the thin adapter architecture:
- This behavior focuses exclusively on message processing logic
- The connection layer delegates message handling responsibilities to implementations
- Your implementation can use domain-specific message types and validation rules
- The adapter handles encoding/decoding between your domain types and the wire format
Delegation Flow
The message handling delegation flow works as follows:
- Raw frames are received by the connection handler
- Text/binary frames are passed to your
handle_message/2
callback - Your implementation processes the message according to your application's needs
- If you need to send a response, the adapter handles the encoding back to wire format
Implementation Example
defmodule MyApp.ChatMessageHandler do
@behaviour WebsockexNova.Behaviors.MessageHandler
@impl true
def handle_message(%{"type" => "chat_message", "text" => text, "user" => user}, state) do
# Process a chat message
IO.puts("\#{user}: \#{text}")
# Send an acknowledgment
{:reply, {:ack, %{message_id: state.last_message_id}}, state}
end
@impl true
def handle_message(%{"type" => "presence_update", "user" => user, "status" => status}, state) do
# Process a presence update
new_state = update_in(state.users[user], fn _ -> status end)
{:ok, new_state}
end
@impl true
def validate_message(message) when is_map(message) and map_size(message) > 0 do
# Validate that message has a type field
case Map.has_key?(message, "type") do
true -> {:ok, message}
false -> {:error, :missing_type_field, message}
end
end
@impl true
def validate_message(message) do
{:error, :invalid_message_format, message}
end
@impl true
def message_type(%{"type" => type}) when is_binary(type) do
String.to_atom(type)
end
@impl true
def message_type(_message) do
:unknown
end
@impl true
def encode_message({:ack, %{message_id: id}}, _state) do
json = Jason.encode!(%{type: "ack", message_id: id})
{:ok, :text, json}
end
@impl true
def encode_message({:error, reason}, _state) do
json = Jason.encode!(%{type: "error", reason: reason})
{:ok, :text, json}
end
end
Callbacks
message_init/1
- Initialize the handler's statehandle_message/2
- Process an incoming messagevalidate_message/1
- Validate message format and contentmessage_type/1
- Extract or determine the message typeencode_message/2
- Encode a message for sending
Summary
Types
Return values for message encoding
Frame type
Return values for message handling callbacks
Message content
Message type
Handler state
Return values for message validation
Callbacks
Encode a message for sending.
Process an incoming message.
Initialize the handler's state.
Determine the type of a message.
Validate an incoming message.
Types
@type encode_return() :: {:ok, frame_type(), binary()} | {:error, term()}
Return values for message encoding
{:ok, frame_type, data}
- Successfully encoded messageframe_type
- The WebSocket frame type (:text, :binary, etc.)data
- The encoded message data as binary
{:error, reason}
- Failed to encode message
Note: When returning binary data, you may use either:
{:ok, :binary, my_binary_data}
for explicit binary frames{:ok, :text, my_text_data}
for text frames
@type frame_type() :: :text | :binary | :ping | :pong | :close
Frame type
@type handler_return() :: {:ok, state()} | {:reply, message_type(), state()} | {:reply_many, [message_type()], state()} | {:close, integer(), String.t(), state()} | {:error, term(), state()}
Return values for message handling callbacks
{:ok, new_state}
- Continue with the updated state{:reply, message_type, new_state}
- Send a message and continue{:reply_many, [message_type], new_state}
- Send multiple messages{:close, code, reason, new_state}
- Close the connection{:error, reason, new_state}
- Error occurred during processing
Message content
Message type
@type state() :: map()
Handler state
Return values for message validation
{:ok, message}
- Message is valid, possibly normalized{:error, reason, message}
- Message is invalid with reason
Callbacks
@callback encode_message(message_type(), state()) :: encode_return()
Encode a message for sending.
Called to convert a message into a WebSocket frame.
Parameters
message_type
- The type of message to encodestate
- Current handler state
Returns
{:ok, frame_type, data}
- Successfully encoded messageframe_type
- The WebSocket frame type (:text, :binary, etc.)data
- The encoded message data as binary
{:error, reason}
- Failed to encode message
Note: When returning binary data, you may use either:
{:ok, :binary, my_binary_data}
for explicit binary frames{:ok, :text, my_text_data}
for text frames
@callback handle_message(message(), state()) :: handler_return()
Process an incoming message.
Called when a message is received from the server.
Parameters
message
- The parsed message (typically a map from decoded JSON)state
- Current handler state
Returns
{:ok, new_state}
- Continue with the updated state{:reply, message_type, new_state}
- Send a message and continue{:reply_many, [message_type], new_state}
- Send multiple messages{:close, code, reason, new_state}
- Close the connection{:error, reason, new_state}
- Error occurred during processing
Initialize the handler's state.
Called when the message handler is started. The return value becomes the initial state.
Parameters
opts
- The options passed to the handler
Returns
{:ok, state}
- The initialized state{:error, reason}
- Initialization failed
@callback message_type(message()) :: message_type()
Determine the type of a message.
Called to extract or determine the type/category of a message.
Parameters
message
- The message to analyze
Returns
- The message type (atom, string, or tuple)
@callback validate_message(message()) :: validate_return()
Validate an incoming message.
Called to validate the format and content of a message.
Parameters
message
- The message to validate
Returns
{:ok, message}
- Message is valid, possibly normalized{:error, reason, message}
- Message is invalid with reason