Minch behaviour (Minch v0.2.1)

View Source

A WebSocket client build on top of Mint.WebSocket.

Features

  • Reconnects with backoff
  • Handling control frames
    • Closes the connection after receiving a :close frame.
    • Replies to server pings automatically.

Installation

The package can be installed by adding minch to your list of dependencies in mix.exs:

def deps do
  [
    {:minch, "~> 0.2.1"}
  ]
end

Usage

Basic example

defmodule EchoClient do
  use Minch

  @impl true
  def connect(state) do
    state.url
  end

  @impl true
  def handle_frame(frame, state) do
    IO.inspect(frame)
    {:ok, state}
  end
end

{:ok, pid} = Minch.start_link(EchoClient, %{url: "wss://ws.postman-echo.com/raw"})
Minch.send_frame(pid, :ping)

Supervised client

defmodule EchoClient do
  use Minch

  require Logger

  def start_link(init_arg) do
    Minch.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @impl true
  def init(_init_arg) do
    {:ok, %{connected?: false}}
  end

  @impl true
  def connect(_state) do
    url = "wss://ws.postman-echo.com/raw"
    headers = [{"authorization", "bearer: example"}]
    # don't do this in production
    options = [transport_opts: [{:verify, :verify_none}]]
    {url, headers, options}
  end

  @impl true
  def handle_connect(_response, state) do
    Logger.info("connected")
    Process.send_after(self(), :produce, 5000)
    {:reply, {:text, "welcome"}, %{state | connected?: true}}
  end

  @impl true
  def handle_disconnect(reason, attempt, state) do
    Logger.warning("disconnected: #{inspect(reason)}")
    {:reconnect, Minch.backoff(attempt), %{state | connected?: false}}
  end

  @impl true
  def handle_info(:produce, state) do
    Process.send_after(self(), :produce, 5000)
    {:reply, {:text, DateTime.utc_now() |> DateTime.to_iso8601()}, state}
  end

  @impl true
  def handle_frame(frame, state) do
    Logger.info(inspect(frame))
    {:ok, state}
  end
end

Simple client

url = "wss://ws.postman-echo.com/raw"
headers = []
# don't do this in production
options = [transport_opts: [{:verify, :verify_none}]]

IO.puts("checking ping to #{url}...")

case Minch.connect(url, headers, options) do
  {:ok, pid, ref} ->
    Minch.send_frame(pid, {:text, to_string(System.monotonic_time())})

    case Minch.receive_frame(ref, 5000) do
      {:text, start} ->
        ping =
          System.convert_time_unit(
            System.monotonic_time() - String.to_integer(start),
            :native,
            :millisecond
          )

        IO.puts("#{ping}ms")

      :timeout ->
        IO.puts("timeout")
    end

    Minch.close(pid)

  {:error, error} ->
    IO.puts("connection error: #{inspect(error)}")
end

Summary

Callbacks

Invoked to retrieve the connection details.

Invoked to handle a successful connection.

Invoked to handle a disconnect from the server or a failed connection attempt.

Invoked to handle internal errors.

Invoked to handle an incoming WebSocket frame.

Invoked to handle info messages.

Invoked when the client process is started.

Invoked when the client process is about to exit.

Functions

Calculates an exponential backoff duration.

Closes the connection opened by connect/3.

Connects to a WebSocket server.

Receives an incoming WebSocket frame.

Sends a WebSocket frame.

Starts a Minch client process linked to the current process.

Types

callback_result()

@type callback_result() ::
  {:ok, state()}
  | {:reply, frame() | [frame()], state()}
  | {:close, code :: non_neg_integer(), reason :: binary(), state()}
  | {:stop, reason :: term(), state()}

client()

@type client() :: GenServer.server()

frame()

response()

@type response() :: %{status: Mint.Types.status(), headers: Mint.Types.headers()}

state()

@type state() :: term()

Callbacks

connect(state)

@callback connect(state :: state()) :: url | {url, headers} | {url, headers, options}
when url: String.t() | URI.t(),
     headers: Mint.Types.headers(),
     options: Keyword.t()

Invoked to retrieve the connection details.

See options for Mint.HTTP.connect/4 and Mint.WebSocket.upgrade/5.

handle_connect(response, state)

@callback handle_connect(response(), state()) :: callback_result()

Invoked to handle a successful connection.

handle_disconnect(reason, attempt, state)

@callback handle_disconnect(reason :: term(), attempt :: pos_integer(), state()) ::
  {:reconnect, backoff :: pos_integer(), state()}
  | {:stop, reason :: term(), state()}

Invoked to handle a disconnect from the server or a failed connection attempt.

Returning {:reconnect, backoff, state} will schedule a reconnect after backoff milliseconds.

handle_error(error, state)

@callback handle_error(error :: term(), state()) :: callback_result()

Invoked to handle internal errors.

handle_frame(frame, state)

@callback handle_frame(Mint.WebSocket.frame(), state()) :: callback_result()

Invoked to handle an incoming WebSocket frame.

handle_info(message, state)

@callback handle_info(message :: term(), state()) :: callback_result()

Invoked to handle info messages.

init(init_arg)

@callback init(init_arg :: term()) :: {:ok, state()}

Invoked when the client process is started.

terminate(reason, state)

@callback terminate(reason, state :: state()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term()

Invoked when the client process is about to exit.

Functions

backoff(attempt, opts \\ [])

@spec backoff(non_neg_integer(),
  min: pos_integer(),
  max: pos_integer(),
  factor: pos_integer()
) ::
  non_neg_integer()

Calculates an exponential backoff duration.

It accepts the following options:

  • :min - The minimum backoff duration in milliseconds. Defaults to 200.
  • :max - The maximum backoff duration in milliseconds. Defaults to 30_000.
  • :factor - The exponent to apply to the attempt number. Defaults to 2.

Examples

iex(8)> for attempt <- 1..20, do: Minch.backoff(attempt)
[200, 800, 1800, 3200, 5000, 7200, 9800, 12800, 16200, 20000, 24200, 28800,
30000, 30000, 30000, 30000, 30000, 30000, 30000, 30000]

for attempt <- 1..20, do: Minch.backoff(attempt, min: 100, max: 10000, factor: 1.5)
[100, 283, 520, 800, 1118, 1470, 1852, 2263, 2700, 3162, 3648, 4157, 4687, 5238,
 5809, 6400, 7009, 7637, 8282, 8944]

close(pid)

@spec close(pid()) :: :ok

Closes the connection opened by connect/3.

connect(url, headers \\ [], options \\ [])

@spec connect(String.t() | URI.t(), Mint.Types.headers(), Keyword.t()) ::
  {:ok, pid(), Mint.Types.request_ref()} | {:error, Mint.WebSocket.error()}

Connects to a WebSocket server.

Once connected, the function will return the connection's PID and the request reference.

Use the send_frame/2 function to send WebSocket frames to the server.

Incoming WebSocket frames will be sent to the caller's mailbox as a tuple {:frame, request_ref, frame}. You can use the receive_frame/2 function to receive these frames.

Use Process.monitor/1 to get notified when the connection is closed by the server.

Example

iex> {:ok, pid, ref} = Minch.connect("wss://ws.postman-echo.com/raw", [], transport_opts: [{:verify, :verify_none}])
{:ok, _pid, _ref}
iex> Minch.send_frame(pid, {:text, "hello"})
:ok
iex> Minch.receive_frame(ref, 5000)
{:text, "hello"}
iex> Minch.close(pid)
:ok

receive_frame(ref, timeout \\ 5000)

@spec receive_frame(Mint.Types.request_ref(), timeout()) ::
  Mint.WebSocket.frame() | :timeout

Receives an incoming WebSocket frame.

See connect/3.

send_frame(client, frame)

@spec send_frame(client(), Mint.WebSocket.frame() | Mint.WebSocket.shorthand_frame()) ::
  :ok | {:error, term()}

Sends a WebSocket frame.

start_link(module, init_arg, opts \\ [])

@spec start_link(module(), term(), GenServer.options()) :: GenServer.on_start()

Starts a Minch client process linked to the current process.