Skip to content

Commit

Permalink
Support for transient frame updates
Browse files Browse the repository at this point in the history
  • Loading branch information
jonatanklosko committed Mar 21, 2023
1 parent 6a507a3 commit 0139fc3
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 20 deletions.
20 changes: 18 additions & 2 deletions lib/kino/bridge.ex
Expand Up @@ -32,14 +32,30 @@ defmodule Kino.Bridge do
end

@doc """
Sends the given output as intermediate evaluation result to
a specific client.
Sends the given output as intermediate evaluation result directly
to a specific client.
"""
@spec put_output_to(term(), Kino.Output.t()) :: :ok | {:error, request_error()}
def put_output_to(client_id, output) do
with {:ok, reply} <- io_request({:livebook_put_output_to, client_id, output}), do: reply
end

@doc """
Sends the given output as intermediate evaluation result directly
to all connected client.
"""
@spec put_output_to_clients(Kino.Output.t()) :: :ok | {:error, request_error()}
def put_output_to_clients(output) do
io_request_result =
with {:error, :unsupported} <-
io_request({:livebook_put_output_to_clients, output}),
# Livebook v0.8.0 doesn't support direct clients output,
# so we fallback to a regular one
do: io_request({:livebook_put_output, output})

with {:ok, reply} <- io_request_result, do: reply
end

@doc """
Requests the current value of input with the given id.
Expand Down
67 changes: 50 additions & 17 deletions lib/kino/frame.ex
Expand Up @@ -64,11 +64,34 @@ defmodule Kino.Frame do
option is useful when updating frame in response to client
events, such as form submission
* `:history` - if the update will be part of the frame history.
Defaults to `true`, unless `:to` is given. Direct updates are
never a part of the history
"""
@spec render(t(), term(), keyword()) :: :ok
def render(frame, term, opts \\ []) do
opts = Keyword.validate!(opts, [:to])
GenServer.cast(frame.pid, {:render, term, opts[:to]})
opts = Keyword.validate!(opts, [:to, :history])
destination = update_destination_from_opts!(opts)
GenServer.cast(frame.pid, {:render, term, destination})
end

defp update_destination_from_opts!(opts) do
if to = opts[:to] do
if opts[:history] do
raise ArgumentError,
"direct updates sent via :to are never part of the frame history," <>
" passing :history is not supported"
end

{:client, to}
else
if Keyword.get(opts, :history, true) do
:default
else
:clients
end
end
end

@doc """
Expand All @@ -80,11 +103,16 @@ defmodule Kino.Frame do
option is useful when updating frame in response to client
events, such as form submission
* `:history` - if the update will be part of the frame history.
Defaults to `true`, unless `:to` is given. Direct updates are
never a part of the history
"""
@spec append(t(), term(), keyword()) :: :ok
def append(frame, term, opts \\ []) do
opts = Keyword.validate!(opts, [:to])
GenServer.cast(frame.pid, {:append, term, opts[:to]})
opts = Keyword.validate!(opts, [:to, :history])
destination = update_destination_from_opts!(opts)
GenServer.cast(frame.pid, {:append, term, destination})
end

@doc """
Expand All @@ -96,11 +124,16 @@ defmodule Kino.Frame do
option is useful when updating frame in response to client
events, such as form submission
* `:history` - if the update will be part of the frame history.
Defaults to `true`, unless `:to` is given. Direct updates are
never a part of the history
"""
@spec clear(t(), keyword()) :: :ok
def clear(frame, opts \\ []) do
opts = Keyword.validate!(opts, [:to])
GenServer.cast(frame.pid, {:clear, opts[:to]})
opts = Keyword.validate!(opts, [:to, :history])
destination = update_destination_from_opts!(opts)
GenServer.cast(frame.pid, {:clear, destination})
end

@doc """
Expand Down Expand Up @@ -132,22 +165,22 @@ defmodule Kino.Frame do
end

@impl true
def handle_cast({:render, term, to}, state) do
def handle_cast({:render, term, destination}, state) do
output = Kino.Render.to_livebook(term)
put_update(to, state.ref, [output], :replace)
put_update(destination, state.ref, [output], :replace)
state = %{state | outputs: [output]}
{:noreply, state}
end

def handle_cast({:append, term, to}, state) do
def handle_cast({:append, term, destination}, state) do
output = Kino.Render.to_livebook(term)
put_update(to, state.ref, [output], :append)
put_update(destination, state.ref, [output], :append)
state = %{state | outputs: [output | state.outputs]}
{:noreply, state}
end

def handle_cast({:clear, to}, state) do
put_update(to, state.ref, [], :replace)
def handle_cast({:clear, destination}, state) do
put_update(destination, state.ref, [], :replace)
state = %{state | outputs: []}
{:noreply, state}
end
Expand Down Expand Up @@ -178,13 +211,13 @@ defmodule Kino.Frame do
end
end

defp put_update(to, ref, outputs, type) do
defp put_update(destination, ref, outputs, type) do
output = Kino.Output.frame(outputs, %{ref: ref, type: type})

if to do
Kino.Bridge.put_output_to(to, output)
else
Kino.Bridge.put_output(output)
case destination do
:default -> Kino.Bridge.put_output(output)
{:client, to} -> Kino.Bridge.put_output_to(to, output)
:clients -> Kino.Bridge.put_output_to_clients(output)
end
end
end
17 changes: 16 additions & 1 deletion lib/kino/test.ex
Expand Up @@ -37,7 +37,8 @@ defmodule Kino.Test do
end

@doc """
Asserts the given output is sent to the given client within `timeout`.
Asserts the given output is sent directly to the given client within
`timeout`.
## Examples
Expand All @@ -51,6 +52,20 @@ defmodule Kino.Test do
end
end

@doc """
Asserts the given output is sent directly to all clients within `timeout`.
## Examples
assert_output_to("client1", {:markdown, "_hey_"})
"""
defmacro assert_output_to_clients(output, timeout \\ 100) do
quote do
assert_receive {:livebook_put_output_to_clients, unquote(output)}, unquote(timeout)
end
end

@doc """
Asserts a `Kino.JS.Live` kino will broadcast an event within
`timeout`.
Expand Down
5 changes: 5 additions & 0 deletions lib/kino/test/group_leader.ex
Expand Up @@ -44,6 +44,11 @@ defmodule Kino.Test.GroupLeader do
:ok
end

defp io_request({:livebook_put_output_to_clients, output}, state) do
send(state.target, {:livebook_put_output_to_clients, output})
:ok
end

defp io_request(:livebook_get_broadcast_target, state) do
{:ok, state.target}
end
Expand Down
17 changes: 17 additions & 0 deletions test/kino/frame_test.exs
Expand Up @@ -18,6 +18,23 @@ defmodule Kino.FrameTest do
assert_output_to("client1", {:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})
end

test "render/2 sends output directly to clients when :history is false" do
frame = Kino.Frame.new()

Kino.Frame.render(frame, 1, history: false)
assert_output_to_clients({:frame, [{:text, "\e[34m1\e[0m"}], %{type: :replace}})
end

test "render/2 raises when :to and :history is given" do
frame = Kino.Frame.new()

assert_raise ArgumentError,
"direct updates sent via :to are never part of the frame history, passing :history is not supported",
fn ->
Kino.Frame.render(frame, 1, to: "client1", history: true)
end
end

test "append/2 formats the given value into output and sends as :append frame" do
frame = Kino.Frame.new()

Expand Down

0 comments on commit 0139fc3

Please sign in to comment.