mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-01 08:16:09 +08:00
Fastlane JS channel events (#993)
* Fastlane JS channel events * Abstract fastlaning to separate concerns
This commit is contained in:
parent
19b777eb4e
commit
7f19afe7af
4 changed files with 87 additions and 37 deletions
|
|
@ -1312,29 +1312,74 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Subscribes the caller to runtime messages under the given topic.
|
Subscribes the caller to runtime messages under the given topic.
|
||||||
|
|
||||||
|
Broadcasted events are encoded using `encoder`, if successful,
|
||||||
|
the message is sent directly to `receiver_pid`, otherwise an
|
||||||
|
`{:encoding_error, error, message}` is sent to the caller.
|
||||||
"""
|
"""
|
||||||
@spec subscribe_to_runtime_events(id(), String.t(), String.t()) :: :ok | {:error, term()}
|
@spec subscribe_to_runtime_events(
|
||||||
def subscribe_to_runtime_events(session_id, topic, subtopic) do
|
id(),
|
||||||
Phoenix.PubSub.subscribe(Livebook.PubSub, runtime_messages_topic(session_id, topic, subtopic))
|
String.t(),
|
||||||
|
String.t(),
|
||||||
|
(term() -> {:ok, term()} | {:error, term()}),
|
||||||
|
pid()
|
||||||
|
) :: :ok | {:error, term()}
|
||||||
|
def subscribe_to_runtime_events(session_id, topic, subtopic, encoder, receiver_pid) do
|
||||||
|
full_topic = runtime_messages_topic(session_id, topic, subtopic)
|
||||||
|
Phoenix.PubSub.subscribe(Livebook.PubSub, full_topic, metadata: {encoder, receiver_pid})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Unsubscribes the caller from runtime messages subscribed earlier
|
Unsubscribes the caller from runtime messages subscribed earlier
|
||||||
with `subscribe_to_runtime_events/3`.
|
with `subscribe_to_runtime_events/5`.
|
||||||
"""
|
"""
|
||||||
@spec unsubscribe_from_runtime_events(id(), String.t(), String.t()) :: :ok | {:error, term()}
|
@spec unsubscribe_from_runtime_events(id(), String.t(), String.t()) :: :ok | {:error, term()}
|
||||||
def unsubscribe_from_runtime_events(session_id, topic, subtopic) do
|
def unsubscribe_from_runtime_events(session_id, topic, subtopic) do
|
||||||
Phoenix.PubSub.unsubscribe(
|
full_topic = runtime_messages_topic(session_id, topic, subtopic)
|
||||||
Livebook.PubSub,
|
Phoenix.PubSub.unsubscribe(Livebook.PubSub, full_topic)
|
||||||
runtime_messages_topic(session_id, topic, subtopic)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def runtime_messages_topic(session_id, topic, subtopic) do
|
def broadcast_runtime_event(session_id, topic, subtopic, message) do
|
||||||
|
full_topic = runtime_messages_topic(session_id, topic, subtopic)
|
||||||
|
Phoenix.PubSub.broadcast(Livebook.PubSub, full_topic, message, __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp runtime_messages_topic(session_id, topic, subtopic) do
|
||||||
"sessions:#{session_id}:runtime_messages:#{topic}:#{subtopic}"
|
"sessions:#{session_id}:runtime_messages:#{topic}:#{subtopic}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
# Custom dispatcher for broadcasting runtime events
|
||||||
|
def dispatch(subscribers, from, message) do
|
||||||
|
Enum.reduce(subscribers, %{}, fn
|
||||||
|
{pid, _}, cache when pid == from ->
|
||||||
|
cache
|
||||||
|
|
||||||
|
{pid, {encoder, receiver_pid}}, cache ->
|
||||||
|
case cache do
|
||||||
|
%{^encoder => encoded_message} ->
|
||||||
|
send(receiver_pid, encoded_message)
|
||||||
|
cache
|
||||||
|
|
||||||
|
%{} ->
|
||||||
|
case encoder.(message) do
|
||||||
|
{:ok, encoded_message} ->
|
||||||
|
send(receiver_pid, encoded_message)
|
||||||
|
Map.put(cache, encoder, encoded_message)
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
send(pid, {:encoding_error, error, message})
|
||||||
|
cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{pid, _}, cache ->
|
||||||
|
send(pid, message)
|
||||||
|
cache
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Determines locator of the evaluation that the given
|
Determines locator of the evaluation that the given
|
||||||
cell depends on.
|
cell depends on.
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ defmodule Livebook.Session.Worker do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:runtime_broadcast, topic, subtopic, message}, state) do
|
def handle_info({:runtime_broadcast, topic, subtopic, message}, state) do
|
||||||
full_topic = Livebook.Session.runtime_messages_topic(state.session_id, topic, subtopic)
|
Livebook.Session.broadcast_runtime_event(state.session_id, topic, subtopic, message)
|
||||||
Phoenix.PubSub.broadcast(Livebook.PubSub, full_topic, message)
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,13 @@ defmodule LivebookWeb.JSOutputChannel do
|
||||||
socket = assign(socket, ref_with_pid: ref_with_pid, ref_with_count: ref_with_count)
|
socket = assign(socket, ref_with_pid: ref_with_pid, ref_with_count: ref_with_count)
|
||||||
|
|
||||||
if socket.assigns.ref_with_count[ref] == 1 do
|
if socket.assigns.ref_with_count[ref] == 1 do
|
||||||
Livebook.Session.subscribe_to_runtime_events(socket.assigns.session_id, "js_live", ref)
|
Livebook.Session.subscribe_to_runtime_events(
|
||||||
|
socket.assigns.session_id,
|
||||||
|
"js_live",
|
||||||
|
ref,
|
||||||
|
&fastlane_encoder/1,
|
||||||
|
socket.transport_pid
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -52,8 +58,8 @@ defmodule LivebookWeb.JSOutputChannel do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:connect_reply, data, %{ref: ref}}, socket) do
|
def handle_info({:connect_reply, payload, %{ref: ref}}, socket) do
|
||||||
with {:error, error} <- try_push(socket, "init:#{ref}", nil, data) do
|
with {:error, error} <- try_push(socket, "init:#{ref}", nil, payload) do
|
||||||
message = "Failed to serialize initial widget data, " <> error
|
message = "Failed to serialize initial widget data, " <> error
|
||||||
push(socket, "error:#{ref}", %{"message" => message})
|
push(socket, "error:#{ref}", %{"message" => message})
|
||||||
end
|
end
|
||||||
|
|
@ -61,20 +67,24 @@ defmodule LivebookWeb.JSOutputChannel do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:event, event, payload, %{ref: ref}}, socket) do
|
def handle_info({:encoding_error, error, {: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
|
||||||
message = "Failed to serialize event payload, " <> error
|
push(socket, "error:#{ref}", %{"message" => message})
|
||||||
push(socket, "error:#{ref}", %{"message" => message})
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
# In case the payload fails to encode we catch the error
|
|
||||||
defp try_push(socket, event, meta, payload) do
|
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
|
try do
|
||||||
payload = transport_encode!(meta, payload)
|
{:ok, fun.()}
|
||||||
push(socket, event, payload)
|
|
||||||
catch
|
catch
|
||||||
:error, %Protocol.UndefinedError{protocol: Jason.Encoder, value: value} ->
|
:error, %Protocol.UndefinedError{protocol: Jason.Encoder, value: value} ->
|
||||||
{:error, "value #{inspect(value)} is not JSON-serializable, use another data type"}
|
{:error, "value #{inspect(value)} is not JSON-serializable, use another data type"}
|
||||||
|
|
@ -84,6 +94,16 @@ defmodule LivebookWeb.JSOutputChannel do
|
||||||
end
|
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_output",
|
||||||
|
event: "event:#{ref}",
|
||||||
|
payload: transport_encode!([event], payload)
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
# A user payload can be either a JSON-serializable term
|
# A user payload can be either a JSON-serializable term
|
||||||
# or a {:binary, info, binary} tuple, where info is a
|
# or a {:binary, info, binary} tuple, where info is a
|
||||||
# JSON-serializable term. The channel allows for sending
|
# JSON-serializable term. The channel allows for sending
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,6 @@ defmodule LivebookWeb.JSOutputChannelTest do
|
||||||
assert_push "init:1", %{"root" => [nil, [1, 2, 3]]}
|
assert_push "init:1", %{"root" => [nil, [1, 2, 3]]}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sends events received from widget server to the client", %{socket: socket} do
|
|
||||||
send(socket.channel_pid, {:event, "ping", [1, 2, 3], %{ref: "1"}})
|
|
||||||
|
|
||||||
assert_push "event:1", %{"root" => [["ping"], [1, 2, 3]]}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends client events to the corresponding widget server", %{socket: socket} do
|
test "sends client events to the corresponding widget server", %{socket: socket} do
|
||||||
push(socket, "connect", %{"session_token" => session_token(), "ref" => "1"})
|
push(socket, "connect", %{"session_token" => session_token(), "ref" => "1"})
|
||||||
|
|
||||||
|
|
@ -51,14 +45,6 @@ defmodule LivebookWeb.JSOutputChannelTest do
|
||||||
assert_push "init:1", {:binary, <<24::size(32), "[null,{\"message\":\"hey\"}]", 1, 2, 3>>}
|
assert_push "init:1", {:binary, <<24::size(32), "[null,{\"message\":\"hey\"}]", 1, 2, 3>>}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "from server to client", %{socket: socket} do
|
|
||||||
payload = {:binary, %{message: "hey"}, <<1, 2, 3>>}
|
|
||||||
send(socket.channel_pid, {:event, "ping", payload, %{ref: "1"}})
|
|
||||||
|
|
||||||
assert_push "event:1",
|
|
||||||
{:binary, <<28::size(32), "[[\"ping\"],{\"message\":\"hey\"}]", 1, 2, 3>>}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "form client to server", %{socket: socket} do
|
test "form client to server", %{socket: socket} do
|
||||||
push(socket, "connect", %{"session_token" => session_token(), "ref" => "1"})
|
push(socket, "connect", %{"session_token" => session_token(), "ref" => "1"})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue