Repository: livebook-dev/kino
Pull request #274 (2023-03-28 09:35:06)

Support deferred reply in `Kino.JS.Live.handle_call/3`
Fix the wrong `JS.Live.handle_call/3` behavior, described in #273.


Patch:
diff --git a/lib/kino/js/live.ex b/lib/kino/js/live.ex
index 2e0813e..aad6916 100644
--- a/lib/kino/js/live.ex
+++ b/lib/kino/js/live.ex
@@ -190,6 +190,8 @@ defmodule Kino.JS.Live do
 
   @type payload :: term() | {:binary, info :: term(), binary()}
 
+  @type from :: GenServer.from()
+
   @doc """
   Invoked when the server is started.
 
@@ -263,7 +265,7 @@ defmodule Kino.JS.Live do
 
   See `c:GenServer.handle_call/3` for more details.
   """
-  @callback handle_call(msg :: term(), from :: term(), ctx :: Context.t()) ::
+  @callback handle_call(msg :: term(), from(), ctx :: Context.t()) ::
               {:noreply, ctx :: Context.t()} | {:reply, term(), ctx :: Context.t()}
 
   @doc """
@@ -368,6 +370,20 @@ defmodule Kino.JS.Live do
     Kino.JS.Live.Server.call(kino.pid, term, timeout)
   end
 
+  @doc """
+  Replies to the kino server.
+
+  This function can be used to explicitly send a reply to the kino server
+  that called `call/3` when the reply cannot be specified in the return
+  value of `handle_call/3`.
+
+  See `GenServer.reply/2` for more details.
+  """
+  @spec reply(from(), term()) :: :ok
+  def reply(kino, term) do
+    Kino.JS.Live.Server.reply(kino, term)
+  end
+
   @doc """
   Starts monitoring the kino server from the calling process.
 
diff --git a/lib/kino/js/live/server.ex b/lib/kino/js/live/server.ex
index c9f1ccb..f05efbd 100644
--- a/lib/kino/js/live/server.ex
+++ b/lib/kino/js/live/server.ex
@@ -17,6 +17,8 @@ defmodule Kino.JS.Live.Server do
 
   defdelegate call(pid, term, timeout), to: GenServer
 
+  defdelegate reply(client, term), to: GenServer
+
   def broadcast_event(ctx, event, payload) do
     ref = ctx.__private__.ref
     Kino.Bridge.broadcast("js_live", ref, {:event, event, payload, %{ref: ref}})
@@ -47,8 +49,10 @@ defmodule Kino.JS.Live.Server do
 
   @impl true
   def handle_call(msg, from, state) do
-    {:reply, reply, ctx} = state.module.handle_call(msg, from, state.ctx)
-    {:reply, reply, %{state | ctx: ctx}}
+    case state.module.handle_call(msg, from, state.ctx) do
+      {:reply, reply, ctx} -> {:reply, reply, %{state | ctx: ctx}}
+      {:noreply, ctx} -> {:noreply, %{state | ctx: ctx}}
+    end
   end
 
   @impl true


Test patch:
diff --git a/test/kino/js/live_test.exs b/test/kino/js/live_test.exs
index 9bf53c9..31f0cf6 100644
--- a/test/kino/js/live_test.exs
+++ b/test/kino/js/live_test.exs
@@ -25,6 +25,13 @@ defmodule Kino.JS.LiveTest do
       assert count == 1
     end
 
+    test "handle_call/3 :noreply" do
+      kino = LiveCounter.new(0)
+      LiveCounter.bump(kino, 1)
+      count = LiveCounter.read_after(kino, 0)
+      assert count == 1
+    end
+
     test "handle_info/2" do
       kino = LiveCounter.new(0)
       send(kino.pid, {:ping, self()})
diff --git a/test/support/test_modules/live_counter.ex b/test/support/test_modules/live_counter.ex
index 2d40b18..04b6b00 100644
--- a/test/support/test_modules/live_counter.ex
+++ b/test/support/test_modules/live_counter.ex
@@ -14,6 +14,10 @@ defmodule Kino.TestModules.LiveCounter do
     Kino.JS.Live.call(kino, :read)
   end
 
+  def read_after(kino, after_ms) do
+    Kino.JS.Live.call(kino, {:read, after_ms})
+  end
+
   @impl true
   def init(count, ctx) do
     {:ok, assign(ctx, count: count)}
@@ -44,12 +48,24 @@ defmodule Kino.TestModules.LiveCounter do
     {:reply, ctx.assigns.count, ctx}
   end
 
+  @impl true
+  def handle_call({:read, after_ms}, from, ctx) do
+    Process.send_after(self(), {:read_reply, from}, after_ms)
+    {:noreply, ctx}
+  end
+
   @impl true
   def handle_info({:ping, from}, ctx) do
     send(from, :pong)
     {:noreply, ctx}
   end
 
+  @impl true
+  def handle_info({:read_reply, from}, ctx) do
+    Kino.JS.Live.reply(from, ctx.assigns.count)
+    {:noreply, ctx}
+  end
+
   defp bump_count(ctx, by) do
     broadcast_event(ctx, "bump", %{by: by})
     update(ctx, :count, &(&1 + by))