WebsockexNova.Gun.ConnectionWrapper (WebsockexNova v0.1.1)
View SourceA thin adapter over Gun's WebSocket implementation, providing a standardized API.
Thin Adapter Pattern
This module implements the "thin adapter" architectural pattern by:
Abstracting Gun's API: Provides a simpler, more standardized interface over Gun's lower-level functionality while maintaining full access to Gun's capabilities
Minimizing Logic: Acts primarily as a pass-through to the underlying Gun library, with minimal logic in the adapter itself
Delegating Business Logic: Forwards most decisions to specialized modules like ConnectionManager and behavior callbacks
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:
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
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
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
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
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.
Process a Gun message.
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
@type frame() :: {:text, binary()} | {:binary, binary()} | :ping | :pong | :close | {:close, non_neg_integer(), binary()}
WebSocket frame types
@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
@type status() ::
:initialized
| :connecting
| :connected
| :websocket_connected
| :disconnected
| :reconnecting
| :error
Connection status
Functions
@spec authenticate(WebsockexNova.ClientConn.t(), reference(), map()) :: any()
Authenticates using the configured auth handler.
Returns a specification to start this module under a supervisor.
See Supervisor
.
@spec close(WebsockexNova.ClientConn.t()) :: :ok
Closes a WebSocket connection.
Parameters
conn
- The client connection struct
Returns
:ok
@spec get_state(WebsockexNova.ClientConn.t()) :: WebsockexNova.Gun.ConnectionState.t()
Gets the current connection state.
Parameters
conn
- The client connection struct
Returns
- The current state struct
@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 serverport
- Port number of the server (default: 80, or 443 for TLS)path
- WebSocket endpoint pathoptions
- Connection options (seeoptions/0
)
Returns
{:ok, %WebsockexNova.ClientConn{}}
on success{:error, reason}
on failure
@spec ping(WebsockexNova.ClientConn.t(), reference()) :: any()
Sends a ping using the configured connection handler.
@spec process_transport_message(WebsockexNova.ClientConn.t(), tuple()) :: :ok
Process a Gun message.
Parameters
conn
- The client connection structmessage
- The Gun message to process
Returns
:ok
@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:
- Validates that the provided Gun PID exists and is alive
- Creates a monitor for the Gun process
- Retrieves information about the Gun connection using
:gun.info/1
- Sets the current process as the Gun process owner with
:gun.set_owner/2
- 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 structgun_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
@spec send_frame(WebsockexNova.ClientConn.t(), reference(), frame() | [frame()]) :: :ok | {:error, term()}
Sends a WebSocket frame.
Parameters
conn
- The client connection structstream_ref
- The stream reference from the upgradeframe
- WebSocket frame to send
Returns
:ok
on success{:error, reason}
on failure
@spec set_status(WebsockexNova.ClientConn.t(), status()) :: :ok
Sets the connection status (mainly for testing).
Parameters
conn
- The client connection structstatus
- The new status to set
Returns
:ok
@spec status(WebsockexNova.ClientConn.t(), reference()) :: any()
Gets the status using the configured connection handler.
@spec subscribe(WebsockexNova.ClientConn.t(), reference(), String.t(), map()) :: any()
Subscribes to a channel using the configured subscription handler.
@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:
- Validates that the Gun process exists and is alive
- Validates that the target process exists and is alive
- Demonitors the current Gun process monitor
- Creates a new monitor for the Gun process
- Uses
:gun.set_owner/2
to redirect Gun messages to the new owner - Sends a
:gun_info
message with connection state to the new owner - 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 structnew_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
@spec unsubscribe(WebsockexNova.ClientConn.t(), reference(), String.t()) :: any()
Unsubscribes from a channel using the configured subscription handler.
@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 structpath
- The WebSocket endpoint pathheaders
- Additional headers for the upgrade request
Returns
{:ok, reference()}
on success{:error, reason}
on failure
@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 structstream_ref
- Stream reference from the upgradetimeout
- Timeout in milliseconds (default: 5000)
Returns
{:ok, headers}
on successful upgrade{:error, reason}
on failure