Packages

Websocket Library written in Elixir

Current section

Files

Jump to
enchufeweb lib enchufeweb.ex
Raw

lib/enchufeweb.ex

defmodule Enchufeweb do
@moduledoc """
`Enchufeweb` is a websocket client library written in Elixir and based on
the Erlang library [websocket_client](https://hex.pm/packages/websocket_client).
"""
@type frame :: :close | :ping | :pong | binary
@type conn_mode :: :disconnected | :once | :reconnect
@type websocket_req :: map
@type state :: any
@doc """
Callback which will be called when a message is received.
The argument has to be a binary message or one of these atoms: :close, :ping or :pong
Output:
* {:ok, state} : nothing occurs
* {:reply, reply, state} : It will send `reply` to the server.
* {:close, reason, state} : It will close the connection due to `reason`.
"""
@callback handle_message(frame, state) :: {:ok, state}
| {:reply, frame, state}
| {:close, binary, state}
@doc """
Callback which will be called when the connection has been made.
Input:
* Websocket request information
* Current state
Output:
* {:ok, state}
* {:ok, keepalive, state} : `keepalive` will be the interval in ms for sending pings to the server.
* {:reply, reply, state} : It will directly send `reply` to the server.
* {:close, reason, state} : It will close the connection due to `reason`.
"""
@callback handle_connection(websocket_req, state) :: {:ok, state}
| {:ok , number, state}
| {:reply, frame, state}
| {:close, binary, state}
@doc """
Callback which will be called when the connection is closed
Input:
* Websocket request information
* Current state
Output:
* {:ok, state} : The process continues although the connection is closed.
* {:reconnect, state} : It tries to reconnect.
* {:reconnect, delay, state} : It tries to reconnect after `delay` ms.
* {:close, reason, state} : It terminates the process.
"""
@callback handle_disconnection(websocket_req, state) :: {:ok, state}
| {:reconnect, state}
| {:reconnect, integer, state}
| {:close, binary, state}
defmacro __using__(_) do
quote do
@behaviour Enchufeweb
require Logger
@msg_after_conn_time 10
def start_link(args) do
{:ok, url} = Keyword.fetch(args, :url)
:websocket_client.start_link(url, __MODULE__, args, args)
end
def ws_send(ws, message) do
frame = make_frame(message)
:websocket_client.cast(ws, frame)
end
def init(args) do
conn_mode =
with {:ok, ws_opts} <- Keyword.fetch(args, :ws_opts),
{:ok, conn_mode} <- Map.fetch(ws_opts, :conn_mode),
do: conn_mode
mode = if conn_mode == :disconnected, do: :ok, else: conn_mode
:crypto.start()
:ssl.start()
{mode, args}
end
def onconnect(msg, state) do
case handle_connection(msg, state) do
{:reply, reply, new_statte} ->
Process.send_after(self(), make_frame(reply), @msg_after_conn_time)
{:ok, new_statte}
response ->
response
end
end
def ondisconnect(reason, state), do: handle_disconnection(reason, state)
def websocket_info(msg, _conn_state, state), do: {:reply, msg, state}
def websocket_terminate(_msg, _conn_state, _state), do: :ok
def websocket_handle({type, msg}, _conn_state, state) do
data =
cond do
type == :ping -> :ping
type == :pong -> :pong
type == :close -> :close
msg == "" -> type
true -> msg
end
case handle_message(data, state) do
{:reply, reply, new_statte} ->
{:reply, make_frame(reply), new_statte}
{:close, reason, new_statte} ->
{:close, reason, state}
_ ->
{:ok, state}
end
end
defp make_frame(data) do
cond do
is_atom(data) ->
data
is_binary(data) ->
if String.valid?(data), do: {:text, data}, else: {:binary, data}
end
end
end
end
@doc """
It will start (linked) the websocket client.
The argument will be a keyword list:
* url: String. For instance `ws://host:port/endpoint`
* ws_opts: It has to be a map which has to contain the connection mode (`%{conn_mode: conn_mode}`)
* :disconnected : It will begin with the client in a disconnected mode
* :once : It only tries one connection
* :reconnect : It will try to reconnect until it get it
"""
@spec start_link([url: binary, ws_opts: map]) :: {:ok, pid} | {:error, term}
def start_link([url: url, ws_opts: ws_opts]) do
:websocket_client.start_link(url, __MODULE__, ws_opts)
end
@doc """
It will send the given message using the given websocket(pid)
"""
@spec ws_send(pid, frame) :: :ok
def ws_send(ws, message), do: :websocket_client.cast(ws, message)
end