Current section

Files

Jump to
bingex lib bingex socket.ex
Raw

lib/bingex/socket.ex

defmodule Bingex.Socket do
@moduledoc """
Provides an abstraction over WebSocket connections for BingX.
"""
use WebSockex
import Kernel, except: [send: 2]
require Logger
alias :zlib, as: Zlib
@type state() :: term()
@type event() :: term()
@type message() :: binary()
@callback handle_connect(state()) :: {:ok, state()}
@callback handle_disconnect(
details :: WebSockex.connection_status_map(),
state()
) :: {:stop, state()} | {:reconnect, state()}
@callback handle_event(event(), state()) ::
{:ok, state()} | {:disconnect, state()}
@callback handle_call(message(), from :: pid(), state()) ::
{:reply, message :: term(), state()}
| {:ok, state()}
| {:disconnect, state()}
@callback handle_cast(message(), state()) ::
{:send, message(), state()}
| {:ok, state()}
| {:disconnect, state()}
@callback handle_info(
message(),
state()
) ::
{:send, message(), state()}
| {:ok, state()}
| {:disconnect, state()}
@optional_callbacks handle_disconnect: 2,
handle_connect: 1,
handle_call: 3,
handle_info: 2,
handle_cast: 2
# Interface
# =========
@doc """
Starts a WebSocket process linked to the current process.
"""
def start_link(url, module, state, options \\ []) do
WebSockex.start_link(url, __MODULE__, {module, state}, options)
end
@doc """
Starts a WebSocket process.
"""
def start(url, module, state, options \\ []) do
WebSockex.start(url, __MODULE__, {module, state}, options)
end
@doc """
Sends a message to the process to the specified PID.
"""
def send(dest, message) do
WebSockex.cast(dest, {:"$socket_send", message})
end
@doc """
Sends an asynchronous message.
"""
def cast(dest, message) do
WebSockex.cast(dest, message)
end
@doc """
Returns the `pid` of a Socket process, `nil` otherwise.
"""
@spec whereis(dest :: atom() | pid()) :: pid | nil
def whereis(dest)
def whereis(pid) when is_pid(pid), do: pid
def whereis(name) when is_atom(name) do
Process.whereis(name)
end
@doc """
Sends a synchronous message.
"""
def call(dest, message, timeout \\ 5000) do
case whereis(dest) do
nil ->
exit({:noproc, {__MODULE__, :call, [dest, message, timeout]}})
pid ->
ref = make_ref()
WebSockex.cast(pid, {:"$socket_call", {ref, self()}, message})
receive do
{:"$socket_call", ^ref, message} -> message
after
timeout ->
exit({{:timeout, {__MODULE__, :call, [pid, message, timeout]}}, self()})
end
end
end
# Implementation
# ==============
@impl WebSockex
def handle_connect(_conn, {module, state}) do
if function_exported?(module, :handle_connect, 1) do
{:ok, new_state} = apply(module, :handle_connect, [state])
{:ok, {module, new_state}}
else
{:ok, {module, state}}
end
end
@impl WebSockex
def handle_disconnect(details, {module, state}) do
if function_exported?(module, :handle_disconnect, 2) do
case apply(module, :handle_disconnect, [details, state]) do
{:reconnect, new_state} -> {:reconnect, {module, new_state}}
{:stop, new_state} -> {:ok, {module, new_state}}
end
else
{:reconnect, {module, state}}
end
end
@impl WebSockex
def handle_frame({:binary, frame}, {module, state}) do
case Zlib.gunzip(frame) do
"Ping" ->
{:reply, {:text, "Pong"}, {module, state}}
data ->
handle_event_data(data, {module, state})
end
end
@impl WebSockex
def handle_frame(_data, {module, state}) do
{:ok, {module, state}}
end
@impl WebSockex
def handle_cast({:"$socket_call", {ref, from}, message}, {module, state}) do
if function_exported?(module, :handle_call, 3) do
case apply(module, :handle_call, [message, from, state]) do
{:disconnect, new_state} ->
{:close, {module, new_state}}
{:reply, message, new_state} ->
Kernel.send(from, {:"$socket_call", ref, message})
{:ok, {module, new_state}}
{:ok, new_state} ->
{:ok, {module, new_state}}
end
else
{:ok, {module, state}}
end
end
@impl WebSockex
def handle_cast({:"$socket_send", message}, state) do
{:reply, {:text, message}, state}
end
@impl WebSockex
def handle_cast(message, {module, state}) do
if function_exported?(module, :handle_cast, 2) do
case apply(module, :handle_cast, [message, state]) do
{:disconnect, new_state} ->
{:close, {module, new_state}}
{:ok, new_state} ->
{:ok, {module, new_state}}
{:send, message, new_state} ->
{:reply, {:text, message}, {module, new_state}}
end
else
{:ok, {module, state}}
end
end
@impl WebSockex
def handle_info(message, {module, state}) do
if function_exported?(module, :handle_info, 2) do
case apply(module, :handle_info, [message, state]) do
{:disconnect, new_state} ->
{:close, {module, new_state}}
{:ok, new_state} ->
{:ok, {module, new_state}}
{:send, message, new_state} ->
{:reply, {:text, message}, {module, new_state}}
end
else
{:ok, {module, state}}
end
end
# Helpers
# =======
defp handle_event_data(data, {module, state}) do
case send_event_data(data, {module, state}) do
{:ok, new_state} -> {:ok, {module, new_state}}
{:disconnect, new_state} -> {:close, {module, new_state}}
end
end
defp send_event_data(data, {module, state}) do
case JSON.decode(data) do
{:ok, event} ->
apply(module, :handle_event, [event, state])
{:error, reason} ->
Logger.warning(
"Could not decode event as JSON: #{inspect(data)}. " <>
"Reason: #{inspect(reason)}"
)
end
end
end