defmodule Chaperon.Action.WebSocket.Connect do
@moduledoc """
WebSocket connection action that needs to be run in order for a
`Chaperon.Session` to be successfully connected to a web server via WebSocket.
Assigns `websocket` and `websocket_url` values to a session when run,
which are used by the remaining websocket actions found under
`Chaperon.Action.WebSocket`.
The stored values are accessible via `session.assigned.websocket` &
`session.assigned.websocket_url`.
"""
defstruct path: nil,
options: []
def url(action, session) do
case Chaperon.Action.HTTP.url(action, session) do
"https" <> rest ->
"wss" <> rest
"http" <> rest ->
"ws" <> rest
"ws" <> _ = ws_url ->
ws_url
end
end
end
defimpl Chaperon.Actionable, for: Chaperon.Action.WebSocket.Connect do
alias Chaperon.Session
alias Chaperon.Action.WebSocket
alias Chaperon.Action.WebSocket.Connect
import Chaperon.Timing, only: [seconds: 1]
use Chaperon.Session.Logging
def run(action, session) do
ws_url = Connect.url(action, session)
session
|> log_info("WS_CONN #{ws_url}")
timeout = Session.timeout(session)
async_connect(session, ws_url)
receive do
{:ws_connected, ws_client, ^ws_url} ->
session
|> WebSocket.assign_for_action(action, ws_client, ws_url)
|> Session.ok()
{:ws_closed, ^ws_url} ->
session
|> log_error("Failed to connect via WS to #{ws_url} - Connection closed remotely")
{:ws_failed, ^ws_url, error = %WebSockex.RequestError{code: code, message: message}} ->
session
|> log_error(
"Failed to connect via WS to #{ws_url} - Failed connection request response: #{code} : #{
message
}"
)
|> Session.error({:ws_failed, ws_url, error})
after
timeout ->
session
|> log_info("Timeout while connecting via WS to #{ws_url}")
|> Session.error({:timeout, :ws_conn, ws_url, timeout})
end
end
def abort(action, session) do
{:ok, action, session}
end
def async_connect(session, ws_url) do
parent = self()
spawn_link(fn ->
session
|> connection_attempt_loop(ws_url, parent)
end)
end
defp connection_attempt_loop(session, ws_url, parent) do
case WebSocket.Client.start_link(session, ws_url) do
{:ok, ws_client} ->
send(parent, {:ws_connected, ws_client, ws_url})
{:error, %WebSockex.ConnError{original: :closed}} ->
send(parent, {:ws_closed, ws_url})
{:error, %WebSockex.ConnError{original: :timeout}} ->
session
|> log_warn("Failed to connect via WS to #{ws_url} - TIMEOUT")
|> Session.random_delay(
session
|> Session.config("ws.connect_timeout", 3 |> seconds)
)
|> connection_attempt_loop(ws_url, parent)
{:error, req_err = %WebSockex.RequestError{}} ->
send(parent, {:ws_failed, ws_url, req_err})
end
end
end
defimpl String.Chars, for: Chaperon.Action.WebSocket.Connect do
def to_string(%{path: path}), do: "WS Connect[#{path}]"
end