Packages
livebook
0.8.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/channels/js_view_channel.ex
defmodule LivebookWeb.JSViewChannel do
use Phoenix.Channel
@impl true
def join("js_view", %{"session_id" => session_id, "client_id" => client_id}, socket) do
{:ok, assign(socket, session_id: session_id, client_id: client_id, ref_with_info: %{})}
end
@impl true
def handle_in("connect", %{"session_token" => session_token, "ref" => ref, "id" => id}, socket) do
{:ok, data} = Phoenix.Token.verify(LivebookWeb.Endpoint, "js view", session_token)
%{pid: pid} = data
send(pid, {:connect, self(), %{origin: socket.assigns.client_id, ref: ref}})
socket =
update_in(socket.assigns.ref_with_info[ref], fn
nil -> %{pid: pid, 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)
pid = socket.assigns.ref_with_info[ref].pid
send(pid, {:event, event, payload, %{origin: socket.assigns.client_id, ref: ref}})
{:noreply, socket}
end
def handle_in("ping", %{"ref" => ref}, socket) do
pid = socket.assigns.ref_with_info[ref].pid
send(pid, {:ping, self(), nil, %{ref: ref}})
{:noreply, socket}
end
def handle_in("disconnect", %{"ref" => ref}, socket) do
socket =
if socket.assigns.ref_with_info[ref].count == 1 do
Livebook.Session.unsubscribe_from_runtime_events(
socket.assigns.session_id,
"js_live",
ref
)
{_, socket} = pop_in(socket.assigns.ref_with_info[ref])
socket
else
update_in(socket.assigns.ref_with_info[ref], &%{&1 | count: &1.count - 1})
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}", nil, 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({: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: Jason.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, encode!([meta, info], binary)}
end
defp transport_encode!(meta, payload) do
%{"root" => [meta, payload]}
end
defp transport_decode!({:binary, raw}) do
{[meta, info], binary} = decode!(raw)
{meta, {:binary, info, binary}}
end
defp transport_decode!(raw) do
%{"root" => [meta, payload]} = raw
{meta, payload}
end
defp encode!(meta, binary) do
meta = Jason.encode!(meta)
meta_size = byte_size(meta)
<<meta_size::size(32), meta::binary, binary::binary>>
end
defp decode!(raw) do
<<meta_size::size(32), meta::binary-size(meta_size), binary::binary>> = raw
meta = Jason.decode!(meta)
{meta, binary}
end
end