WebsockexNova.Gun.ConnectionWrapper (WebsockexNova v0.1.0)

View Source

A thin adapter over Gun's WebSocket implementation, providing a standardized API.

Thin Adapter Pattern

This module implements the "thin adapter" architectural pattern by:

  1. Abstracting Gun's API: Provides a simpler, more standardized interface over Gun's lower-level functionality while maintaining full access to Gun's capabilities

  2. Minimizing Logic: Acts primarily as a pass-through to the underlying Gun library, with minimal logic in the adapter itself

  3. Delegating Business Logic: Forwards most decisions to specialized modules like ConnectionManager and behavior callbacks

  4. Standardizing Interfaces: Exposes a consistent API regardless of underlying transport implementation details

This pattern allows WebsockexNova to potentially support different transport layers in the future while maintaining a consistent API for client applications.

Architecture

ConnectionWrapper uses a clean architecture with strict separation of concerns:

  • Core ConnectionWrapper: Minimal GenServer implementation that routes messages
  • ConnectionState: Immutable state management with structured updates
  • ConnectionManager: Business logic for connection lifecycle and state transitions
  • MessageHandlers: Specialized handlers for different Gun message types
  • BehaviorHelpers: Consistent delegation to behavior callbacks
  • ErrorHandler: Standardized error handling patterns

Delegation Pattern

The module employs a standardized multi-level delegation pattern:

  1. Layer 1: GenServer callbacks receive Gun messages

    def handle_info({:gun_ws, gun_pid, stream_ref, frame}, %{gun_pid: gun_pid} = state) do
      MessageHandlers.handle_websocket_frame(gun_pid, stream_ref, frame, state)
    end
  2. Layer 2: Messages are delegated to specialized MessageHandlers

    # In MessageHandlers module
    def handle_websocket_frame(gun_pid, stream_ref, frame, state) do
      # Process frame, then call behavior callbacks through BehaviorHelpers
      BehaviorHelpers.call_handle_frame(state, frame_type, frame_data, stream_ref)
    end
  3. Layer 3: MessageHandlers call behavior callbacks through BehaviorHelpers

    # In BehaviorHelpers module
    def call_handle_frame(state, frame_type, frame_data, stream_ref) do
      handler_module = Map.get(state.handlers, :connection_handler)
      handler_state = Map.get(state.handlers, :connection_handler_state)
      handler_module.handle_frame(frame_type, frame_data, handler_state)
    end
  4. Layer 4: Results are processed through a consistent handler

    # Back in ConnectionWrapper
    def process_handler_result({:reply, frame_type, data, state, stream_ref}) do
      :gun.ws_send(state.gun_pid, stream_ref, {frame_type, data})
      {:noreply, state}
    end

Ownership Model

Gun connections have a specific ownership model where only one process receives messages from a Gun connection. This module provides a complete ownership transfer protocol:

# Process A - Current owner of Gun connection
WebsockexNova.Gun.ConnectionWrapper.transfer_ownership(wrapper_pid, target_pid)

# Process B - Receiving ownership
WebsockexNova.Gun.ConnectionWrapper.receive_ownership(wrapper_pid, gun_pid)

The transfer protocol carefully manages process monitors, message routing, and state synchronization to ensure reliable handoff between processes.

Usage Examples

Basic Connection

# Open a connection
{:ok, conn} = WebsockexNova.Gun.ConnectionWrapper.open("example.com", 443, %{
  transport: :tls,
  callback_handler: MyApp.WebSocketHandler
})

# Upgrade to WebSocket
{:ok, stream_ref} = WebsockexNova.Gun.ConnectionWrapper.upgrade_to_websocket(conn, "/ws")

# Send a frame
WebsockexNova.Gun.ConnectionWrapper.send_frame(conn, stream_ref, {:text, ~s({"type": "ping"})})

With Custom Handlers

# Configure with custom handlers
{:ok, conn} = WebsockexNova.Gun.ConnectionWrapper.open("example.com", 443, %{
  transport: :tls,
  callback_handler: MyApp.ConnectionHandler,
  message_handler: MyApp.MessageHandler,
  error_handler: MyApp.ErrorHandler
})

Process Transfer

# In process A (current owner)
{:ok, conn} = WebsockexNova.Gun.ConnectionWrapper.open("example.com", 443)
WebsockexNova.Gun.ConnectionWrapper.transfer_ownership(conn, process_b_pid)

# In process B (new owner)
WebsockexNova.Gun.ConnectionWrapper.receive_ownership(my_wrapper_pid, gun_pid)

Summary

Types

WebSocket frame types

Options for connection wrapper

Connection status

Functions

Authenticates using the configured auth handler.

Returns a specification to start this module under a supervisor.

Closes a WebSocket connection.

Gets the current connection state.

Opens a connection to a WebSocket server and upgrades to WebSocket in one step.

Sends a ping using the configured connection handler.

Receives ownership of a Gun connection from another process.

Sends a WebSocket frame.

Sets the connection status (mainly for testing).

Gets the status using the configured connection handler.

Subscribes to a channel using the configured subscription handler.

Transfers ownership of the Gun connection to another process.

Unsubscribes from a channel using the configured subscription handler.

Upgrades an HTTP connection to WebSocket.

Waits for the WebSocket upgrade to complete.

Types

frame()

@type frame() ::
  {:text, binary()}
  | {:binary, binary()}
  | :ping
  | :pong
  | :close
  | {:close, non_neg_integer(), binary()}

WebSocket frame types

options()

@type options() :: %{
  optional(:transport) => :tcp | :tls,
  optional(:transport_opts) => Keyword.t(),
  optional(:protocols) => [:http | :http2 | :socks | :ws],
  optional(:retry) => non_neg_integer() | :infinity,
  optional(:callback_pid) => pid(),
  optional(:ws_opts) => map(),
  optional(:backoff_type) => :linear | :exponential | :jittered,
  optional(:base_backoff) => non_neg_integer(),
  optional(:callback_handler) => module(),
  optional(:message_handler) => module(),
  optional(:error_handler) => module(),
  optional(:rate_limiter) => module()
}

Options for connection wrapper

Possible error atoms returned by API functions:

  • :not_connected — The connection is not established or Gun process is missing
  • :stream_not_found — The provided stream reference does not exist or is closed
  • :no_gun_pid — No Gun process is available for ownership transfer
  • :invalid_target_process — The target process for ownership transfer is invalid or dead
  • :gun_process_not_alive — The Gun process is no longer alive
  • :invalid_gun_pid — The provided Gun PID is invalid or dead
  • :http_error — HTTP upgrade or response error (see tuple for details)
  • :invalid_stream_status — The stream is not in a valid state for the requested operation
  • :terminal_error — A terminal error occurred, preventing reconnection
  • :transition_error — State machine transition failed
  • :reconnect_failed — Reconnection attempt failed

status()

@type status() ::
  :initialized
  | :connecting
  | :connected
  | :websocket_connected
  | :disconnected
  | :reconnecting
  | :error

Connection status

Functions

authenticate(client_conn, stream_ref, credentials)

@spec authenticate(WebsockexNova.ClientConn.t(), reference(), map()) :: any()

Authenticates using the configured auth handler.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

close(client_conn)

@spec close(WebsockexNova.ClientConn.t()) :: :ok

Closes a WebSocket connection.

Parameters

  • conn - The client connection struct

Returns

  • :ok

get_state(client_conn)

Gets the current connection state.

Parameters

  • conn - The client connection struct

Returns

  • The current state struct

open(host, port, path, options \\ %{})

@spec open(binary(), pos_integer(), binary(), options()) ::
  {:ok, WebsockexNova.ClientConn.t()} | {:error, term()}

Opens a connection to a WebSocket server and upgrades to WebSocket in one step.

Parameters

  • host - Hostname or IP address of the server
  • port - Port number of the server (default: 80, or 443 for TLS)
  • path - WebSocket endpoint path
  • options - Connection options (see options/0)

Returns

  • {:ok, %WebsockexNova.ClientConn{}} on success
  • {:error, reason} on failure

ping(client_conn, stream_ref)

@spec ping(WebsockexNova.ClientConn.t(), reference()) :: any()

Sends a ping using the configured connection handler.

process_transport_message(client_conn, message)

@spec process_transport_message(WebsockexNova.ClientConn.t(), tuple()) :: :ok

Process a Gun message.

Parameters

  • conn - The client connection struct
  • message - The Gun message to process

Returns

  • :ok

receive_ownership(client_conn, gun_pid)

@spec receive_ownership(WebsockexNova.ClientConn.t(), pid()) :: :ok | {:error, term()}

Receives ownership of a Gun connection from another process.

This function implements the receiving side of the ownership transfer protocol. It's designed to be used in conjunction with transfer_ownership/2 but can also be used independently to take ownership of any Gun process.

The receive protocol:

  1. Validates that the provided Gun PID exists and is alive
  2. Creates a monitor for the Gun process
  3. Retrieves information about the Gun connection using :gun.info/1
  4. Sets the current process as the Gun process owner with :gun.set_owner/2
  5. Updates the ConnectionWrapper state with the Gun PID, monitor reference, and status

This function is particularly useful when implementing systems where connections need to be dynamically reassigned between processes, such as in worker pools or during process handoffs.

Parameters

  • conn - The client connection struct
  • gun_pid - PID of the Gun process being transferred

Returns

  • :ok on success
  • {:error, :invalid_gun_pid} if the Gun process is invalid or dead
  • {:error, reason} for Gun-specific errors

send_frame(conn, stream_ref, frame)

@spec send_frame(WebsockexNova.ClientConn.t(), reference(), frame() | [frame()]) ::
  :ok | {:error, term()}

Sends a WebSocket frame.

Parameters

  • conn - The client connection struct
  • stream_ref - The stream reference from the upgrade
  • frame - WebSocket frame to send

Returns

  • :ok on success
  • {:error, reason} on failure

set_status(client_conn, status)

@spec set_status(WebsockexNova.ClientConn.t(), status()) :: :ok

Sets the connection status (mainly for testing).

Parameters

  • conn - The client connection struct
  • status - The new status to set

Returns

  • :ok

status(client_conn, stream_ref)

@spec status(WebsockexNova.ClientConn.t(), reference()) :: any()

Gets the status using the configured connection handler.

subscribe(client_conn, stream_ref, channel, params)

@spec subscribe(WebsockexNova.ClientConn.t(), reference(), String.t(), map()) :: any()

Subscribes to a channel using the configured subscription handler.

transfer_ownership(client_conn, new_owner_pid)

@spec transfer_ownership(WebsockexNova.ClientConn.t(), pid()) ::
  :ok | {:error, term()}

Transfers ownership of the Gun connection to another process.

Gun connections have a specific ownership model where only one process receives messages from a Gun connection. This function implements a safe transfer protocol that ensures proper message routing after ownership changes.

The transfer protocol:

  1. Validates that the Gun process exists and is alive
  2. Validates that the target process exists and is alive
  3. Demonitors the current Gun process monitor
  4. Creates a new monitor for the Gun process
  5. Uses :gun.set_owner/2 to redirect Gun messages to the new owner
  6. Sends a :gun_info message with connection state to the new owner
  7. Updates the local state with the new monitor reference

After the transfer completes:

  • The target process will receive all Gun messages
  • The original process maintains its ConnectionWrapper state
  • The ConnectionWrapper maintains its monitor of the Gun process

This function is useful for load balancing, process migration, or implementing more complex ownership strategies.

Parameters

  • conn - The client connection struct
  • new_owner_pid - PID of the process that should become the new owner

Returns

  • :ok on success
  • {:error, :no_gun_pid} if no Gun process exists
  • {:error, :invalid_target_process} if the target process is invalid or dead
  • {:error, :gun_process_not_alive} if the Gun process died
  • {:error, reason} for other Gun-specific errors

unsubscribe(client_conn, stream_ref, channel)

@spec unsubscribe(WebsockexNova.ClientConn.t(), reference(), String.t()) :: any()

Unsubscribes from a channel using the configured subscription handler.

upgrade_to_websocket(client_conn, path, headers)

@spec upgrade_to_websocket(WebsockexNova.ClientConn.t(), binary(), Keyword.t()) ::
  {:ok, reference()} | {:error, term()}

Upgrades an HTTP connection to WebSocket.

Parameters

  • conn - The client connection struct
  • path - The WebSocket endpoint path
  • headers - Additional headers for the upgrade request

Returns

  • {:ok, reference()} on success
  • {:error, reason} on failure

wait_for_websocket_upgrade(client_conn, stream_ref, timeout \\ 5000)

@spec wait_for_websocket_upgrade(
  WebsockexNova.ClientConn.t(),
  reference(),
  non_neg_integer()
) ::
  {:ok, list()} | {:error, term()}

Waits for the WebSocket upgrade to complete.

Parameters

  • conn - The client connection struct
  • stream_ref - Stream reference from the upgrade
  • timeout - Timeout in milliseconds (default: 5000)

Returns

  • {:ok, headers} on successful upgrade
  • {:error, reason} on failure