Packages
livebook
0.9.0
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/livebook/web_socket/client_connection.ex
defmodule Livebook.WebSocket.ClientConnection do
@moduledoc false
use Connection
require Logger
alias Livebook.WebSocket
alias Livebook.WebSocket.Client
@timeout 10_000
@backoff 5_000
defstruct [:url, :listener, :headers, :http_conn, :websocket, :ref, id: 0, reply: %{}]
@doc """
Starts a new WebSocket connection with given URL and headers.
"""
@spec start_link(pid(), String.t(), Mint.Types.headers()) :: GenServer.on_start()
def start_link(listener, url, headers \\ []) do
Connection.start_link(__MODULE__, {listener, url, headers})
end
@doc """
Sends a Request to given WebSocket Server.
"""
@spec send_request(pid(), WebSocket.proto()) :: {atom(), term()}
def send_request(conn, %_struct{} = data) do
Connection.call(conn, {:request, data}, @timeout)
end
## Connection callbacks
@impl true
def init({listener, url, headers}) do
state = %__MODULE__{listener: listener, url: url, headers: headers}
{:connect, :init, state}
end
@impl true
def connect(_, state) do
case Client.connect(state.url, state.headers) do
{:ok, conn, websocket, ref} ->
send(state.listener, {:connect, :ok, :connected})
send(self(), {:loop_ping, ref})
{:ok, %{state | http_conn: conn, ref: ref, websocket: websocket}}
{:transport_error, reason} ->
send(state.listener, {:connect, :error, reason})
{:backoff, @backoff, state}
{:server_error, binary} ->
{:response, %{type: {:error, error}}} = decode_response_or_event(binary)
send(state.listener, {:connect, :error, error.details})
{:backoff, @backoff, state}
end
end
@dialyzer {:nowarn_function, disconnect: 2}
@impl true
def disconnect(info, state) do
case info do
{:close, from} -> Logger.debug("Received close from: #{inspect(from)}")
{:error, :closed} -> Logger.error("Connection closed")
{:error, reason} -> Logger.error("Connection error: #{inspect(reason)}")
end
{:connect, :reconnect, state}
end
## GenServer callbacks
@impl true
def handle_call({:request, data}, caller, state) do
id = state.id
frame = LivebookProto.build_request_frame(data, id)
reply = Map.put(state.reply, id, caller)
case Client.send(state.http_conn, state.websocket, state.ref, frame) do
{:ok, conn, websocket} ->
{:noreply, %{state | http_conn: conn, websocket: websocket, id: id + 1, reply: reply}}
{:error, conn, websocket, reason} ->
{:reply, {:error, reason}, %{state | http_conn: conn, websocket: websocket}}
end
end
@loop_ping_delay 5_000
@impl true
def handle_info({:loop_ping, ref}, state) when ref == state.ref and is_reference(ref) do
case Client.send(state.http_conn, state.websocket, state.ref, :ping) do
{:ok, conn, websocket} ->
Process.send_after(self(), {:loop_ping, state.ref}, @loop_ping_delay)
{:noreply, %{state | http_conn: conn, websocket: websocket}}
{:error, conn, websocket, _reason} ->
{:noreply, %{state | http_conn: conn, websocket: websocket}}
end
end
def handle_info({:loop_ping, _another_ref}, state), do: {:noreply, state}
def handle_info({:tcp_closed, _port} = message, state),
do: handle_websocket_message(message, state)
def handle_info({:tcp, _port, _data} = message, state),
do: handle_websocket_message(message, state)
def handle_info(_message, state), do: {:noreply, state}
# Private
def handle_websocket_message(message, state) do
case Client.receive(state.http_conn, state.ref, state.websocket, message) do
{:ok, conn, websocket, data} ->
state = %{state | http_conn: conn, websocket: websocket}
{:noreply, send_received(data, state)}
{:server_error, conn, websocket, reason} ->
send(state.listener, {:connect, :error, reason})
{:connect, :receive, %{state | http_conn: conn, websocket: websocket}}
end
end
defp send_received([], state), do: state
defp send_received([_ | _] = binaries, state) do
for binary <- binaries, reduce: state do
acc ->
case decode_response_or_event(binary) do
{:response, %{id: -1, type: {:error, %{details: reason}}}} ->
reply_to_all({:error, reason}, acc)
{:response, %{id: id, type: {:error, %{details: reason}}}} ->
reply_to_id(id, {:error, reason}, acc)
{:response, %{id: id, type: {:changeset, %{errors: field_errors}}}} ->
reply_to_id(id, {:changeset_error, to_changeset_errors(field_errors)}, acc)
{:response, %{id: id, type: result}} ->
reply_to_id(id, result, acc)
{:event, %{type: {name, data}}} ->
send(acc.listener, {:event, name, data})
acc
end
end
end
defp to_changeset_errors(field_errors) do
for %{field: field, details: errors} <- field_errors, into: %{} do
{String.to_atom(field), errors}
end
end
defp reply_to_all(message, state) do
for {_id, caller} <- state.reply do
Connection.reply(caller, message)
end
state
end
defp reply_to_id(id, message, state) do
{caller, reply} = Map.pop(state.reply, id)
if caller, do: Connection.reply(caller, message)
%{state | reply: reply}
end
defp decode_response_or_event(data) do
case LivebookProto.Response.decode(data) do
%{type: nil} -> {:event, LivebookProto.Event.decode(data)}
response -> {:response, response}
end
end
end