WebsockexNova.Behaviors.MessageHandler behaviour (WebsockexNova v0.1.0)

View Source

Defines 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 data
  • encode_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:

  1. This behavior focuses exclusively on message processing logic
  2. The connection layer delegates message handling responsibilities to implementations
  3. Your implementation can use domain-specific message types and validation rules
  4. The adapter handles encoding/decoding between your domain types and the wire format

Delegation Flow

The message handling delegation flow works as follows:

  1. Raw frames are received by the connection handler
  2. Text/binary frames are passed to your handle_message/2 callback
  3. Your implementation processes the message according to your application's needs
  4. 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 state
  • handle_message/2 - Process an incoming message
  • validate_message/1 - Validate message format and content
  • message_type/1 - Extract or determine the message type
  • encode_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

encode_return()

@type encode_return() :: {:ok, frame_type(), binary()} | {:error, term()}

Return values for message encoding

  • {:ok, frame_type, data} - Successfully encoded message
    • frame_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

frame_type()

@type frame_type() :: :text | :binary | :ping | :pong | :close

Frame type

handler_return()

@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()

@type message() :: map() | binary()

Message content

message_type()

@type message_type() :: atom() | String.t() | {atom(), term()} | binary()

Message type

state()

@type state() :: map()

Handler state

validate_return()

@type validate_return() :: {:ok, message()} | {:error, term(), message()}

Return values for message validation

  • {:ok, message} - Message is valid, possibly normalized
  • {:error, reason, message} - Message is invalid with reason

Callbacks

encode_message(message_type, state)

@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 encode
  • state - Current handler state

Returns

  • {:ok, frame_type, data} - Successfully encoded message
    • frame_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

handle_message(message, state)

@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

message_init(opts)

@callback message_init(opts :: term()) :: {:ok, state()} | {:error, term()}

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

message_type(message)

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

validate_message(message)

@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