Packages
livebook
0.16.4
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
Current section
Files
Jump to
Current section
Files
lib/livebook_web/channels/js_view_channel.ex
defmodule LivebookWeb.JSViewChannel do
use Phoenix.Channel
alias LivebookWeb.CodecHelpers
@impl true
def join("js_view", %{"session_token" => session_token}, socket) do
case Phoenix.Token.verify(LivebookWeb.Endpoint, "session", session_token) do
{:ok, data} ->
{:ok,
assign(socket,
session_id: data.session_id,
client_id: data.client_id,
ref_with_info: %{}
)}
_error ->
{:error, %{reason: "invalid token"}}
end
end
@impl true
def handle_in("connect", %{"connect_token" => connect_token, "ref" => ref, "id" => id}, socket) do
{:ok, %{pid: pid}} =
Phoenix.Token.verify(LivebookWeb.Endpoint, "js-view-connect", connect_token)
send(pid, {:connect, self(), %{origin: socket.assigns.client_id, ref: ref}})
socket =
update_in(socket.assigns.ref_with_info[ref], fn
nil ->
monitor_ref = Process.monitor(pid, tag: {:connect_down, ref})
%{pid: pid, monitor_ref: monitor_ref, count: 1, connect_queue: [id]}
info ->
%{info | count: info.count + 1, connect_queue: info.connect_queue ++ [id]}
end)
if socket.assigns.ref_with_info[ref].count == 1 do
Livebook.Session.subscribe_to_runtime_events(
socket.assigns.session_id,
"js_live",
ref,
&fastlane_encoder/1,
socket.transport_pid
)
end
{:noreply, socket}
end
def handle_in("event", raw, socket) do
{[event, ref], payload} = transport_decode!(raw)
with %{^ref => info} <- socket.assigns.ref_with_info do
send(info.pid, {:event, event, payload, %{origin: socket.assigns.client_id, ref: ref}})
end
{:noreply, socket}
end
def handle_in("ping", %{"ref" => ref}, socket) do
with %{^ref => info} <- socket.assigns.ref_with_info do
send(info.pid, {:ping, self(), nil, %{ref: ref}})
end
{:noreply, socket}
end
def handle_in("disconnect", %{"ref" => ref}, socket) do
socket =
case socket.assigns.ref_with_info do
%{^ref => %{count: 1, monitor_ref: monitor_ref}} ->
Process.demonitor(monitor_ref, [:flush])
session_id = socket.assigns.session_id
Livebook.Session.unsubscribe_from_runtime_events(session_id, "js_live", ref)
{_, socket} = pop_in(socket.assigns.ref_with_info[ref])
socket
%{^ref => %{count: count}} when count > 1 ->
update_in(socket.assigns.ref_with_info[ref], &%{&1 | count: &1.count - 1})
%{} ->
socket
end
{:noreply, socket}
end
@impl true
def handle_info({:connect_reply, payload, %{ref: ref}}, socket) do
# Multiple connections for the same reference may be establish,
# the replies come sequentially and we dispatch them according
# to the clients queue
{id, socket} =
get_and_update_in(socket.assigns.ref_with_info[ref].connect_queue, fn [id | queue] ->
{id, queue}
end)
with {:error, error} <- try_push(socket, "init:#{ref}:#{id}", [true], payload) do
message = "Failed to serialize initial widget data, " <> error
push(socket, "error:#{ref}", %{"message" => message, "init" => true})
end
{:noreply, socket}
end
def handle_info({{:connect_down, ref}, _ref, :process, _pid, _reason}, socket) do
Livebook.Session.unsubscribe_from_runtime_events(socket.assigns.session_id, "js_live", ref)
{%{connect_queue: ids}, socket} = pop_in(socket.assigns.ref_with_info[ref])
for id <- ids do
:ok = try_push(socket, "init:#{ref}:#{id}", [false], nil)
end
{:noreply, socket}
end
def handle_info({:event, event, payload, %{ref: ref}}, socket) do
with {:error, error} <- try_push(socket, "event:#{ref}", [event], payload) do
message = "Failed to serialize widget data, " <> error
push(socket, "error:#{ref}", %{"message" => message})
end
{:noreply, socket}
end
def handle_info({:pong, _, %{ref: ref}}, socket) do
push(socket, "pong:#{ref}", %{})
{:noreply, socket}
end
def handle_info({:encoding_error, error, {:event, _event, _payload, %{ref: ref}}}, socket) do
message = "Failed to serialize widget data, " <> error
push(socket, "error:#{ref}", %{"message" => message})
{:noreply, socket}
end
defp try_push(socket, event, meta, payload) do
with {:ok, _} <-
run_safely(fn ->
push(socket, event, transport_encode!(meta, payload))
end),
do: :ok
end
# In case the payload fails to encode we catch the error
defp run_safely(fun) do
try do
{:ok, fun.()}
rescue
error ->
case error do
%Protocol.UndefinedError{protocol: JSON.Encoder, value: value} ->
{:error, "value #{inspect(value)} is not JSON-serializable, use another data type"}
error ->
{:error, Exception.message(error)}
end
end
end
defp fastlane_encoder({:event, event, payload, %{ref: ref}}) do
run_safely(fn ->
Phoenix.Socket.V2.JSONSerializer.fastlane!(%Phoenix.Socket.Broadcast{
topic: "js_view",
event: "event:#{ref}",
payload: transport_encode!([event], payload)
})
end)
end
# A user payload can be either a JSON-serializable term
# or a {:binary, info, binary} tuple, where info is a
# JSON-serializable term. The channel allows for sending
# either maps or binaries, so we need to translare the
# payload accordingly
defp transport_encode!(meta, {:binary, info, binary}) do
{:binary, CodecHelpers.encode_annotated_binary!([meta, info], binary)}
end
defp transport_encode!(meta, payload) do
%{"root" => [meta, payload]}
end
defp transport_decode!({:binary, raw}) do
{[meta, info], binary} = CodecHelpers.decode_annotated_binary!(raw)
{meta, {:binary, info, binary}}
end
defp transport_decode!(raw) do
%{"root" => [meta, payload]} = raw
{meta, payload}
end
end