Skip to main content

lib/quickbeam/server.ex

defmodule QuickBEAM.Server do
  @moduledoc false

  @spec send_websocket(map(), term(), term(), term()) :: {:noreply, map()}
  def send_websocket(state, socket_id, kind, payload) do
    case Map.get(state.websockets, socket_id) do
      {pid, _ref} -> GenServer.cast(pid, {:send, kind, payload})
      nil -> :ok
    end

    {:noreply, state}
  end

  @spec close_websocket(map(), term(), term(), term()) :: {:noreply, map()}
  def close_websocket(state, socket_id, code, reason) do
    case Map.get(state.websockets, socket_id) do
      {pid, _ref} -> GenServer.cast(pid, {:close, code, reason})
      nil -> :ok
    end

    {:noreply, state}
  end

  defmacro __using__(_opts) do
    quote do
      @spec call(GenServer.server(), String.t(), list(), keyword()) ::
              QuickBEAM.js_result()
      def call(server, fn_name, args \\ [], opts \\ [])
          when is_binary(fn_name) and is_list(args) do
        timeout_ms = Keyword.get(opts, :timeout, 0)
        GenServer.call(server, {:call, fn_name, args, timeout_ms}, :infinity)
      end

      defp handle_pending_ref(ref, result, state) do
        case Map.pop(state.pending, ref) do
          {nil, _} ->
            {:noreply, state}

          {{from, nil}, pending} ->
            GenServer.reply(from, result)
            {:noreply, %{state | pending: pending}}

          {{from, transform}, pending} ->
            GenServer.reply(from, transform.(result))
            {:noreply, %{state | pending: pending}}
        end
      end

      defp put_pending(state, ref, from, transform \\ nil) do
        %{state | pending: Map.put(state.pending, ref, {from, transform})}
      end

      defp js_error_transform do
        fn
          {:ok, value} -> {:ok, value}
          {:error, value} -> {:error, QuickBEAM.JSError.from_js_value(value)}
        end
      end

      # ── Shared handle_call clauses ──

      @impl true
      def handle_call({:eval, code, timeout_ms}, from, state) do
        ref = nif_eval(state, code, timeout_ms)
        {:noreply, put_pending(state, ref, from, js_error_transform())}
      end

      @impl true
      def handle_call({:call, fn_name, args, timeout_ms}, from, state) do
        ref = nif_call(state, fn_name, args, timeout_ms)
        {:noreply, put_pending(state, ref, from, js_error_transform())}
      end

      @impl true
      def handle_call({:dom_find, selector}, from, state) do
        ref = nif_dom_find(state, selector)
        {:noreply, put_pending(state, ref, from)}
      end

      @impl true
      def handle_call({:dom_find_all, selector}, from, state) do
        ref = nif_dom_find_all(state, selector)
        {:noreply, put_pending(state, ref, from)}
      end

      @impl true
      def handle_call({:dom_text, selector}, from, state) do
        ref = nif_dom_text(state, selector)
        {:noreply, put_pending(state, ref, from)}
      end

      @impl true
      def handle_call(:dom_html, from, state) do
        ref = nif_dom_html(state)
        {:noreply, put_pending(state, ref, from)}
      end

      # memory_usage is NOT shared — Runtime unwraps {:ok, v} → v,
      # Context returns {:ok, map}. Each module implements its own.

      @impl true
      def handle_call(:reset, from, state) do
        ref = nif_reset(state)

        transform = fn
          {:ok, _} -> :ok
          {:error, msg} -> {:error, msg}
        end

        {:noreply, put_pending(state, ref, from, transform)}
      end

      @impl true
      def handle_call({:get_global, name}, from, state) do
        ref = nif_get_global(state, name)
        {:noreply, put_pending(state, ref, from, js_error_transform())}
      end

      @impl true
      def handle_call({:set_global, name, value}, _from, state) do
        nif_set_global(state, name, value)
        {:reply, :ok, state}
      end

      @impl true
      def handle_cast({:send_message, message}, state) do
        nif_send_message(state, message)
        {:noreply, state}
      end

      # ── WebSocket helpers ──

      defp handle_websocket_started(socket_id, pid, state) do
        ref = Process.monitor(pid)
        websockets = Map.put(state.websockets, socket_id, {pid, ref})
        {:noreply, %{state | websockets: websockets}}
      end

      defp pop_websocket(state, ref) do
        case Enum.find(state.websockets, fn {_socket_id, {_pid, monitor_ref}} ->
               monitor_ref == ref
             end) do
          {socket_id, {_pid, _monitor_ref}} ->
            {true, %{state | websockets: Map.delete(state.websockets, socket_id)}}

          nil ->
            {false, state}
        end
      end

      defp shutdown_websockets(state) do
        for {_socket_id, {pid, ref}} <- state.websockets do
          Process.exit(pid, :shutdown)

          receive do
            {:DOWN, ^ref, :process, ^pid, _} -> :ok
          after
            5_000 -> :ok
          end
        end
      end
    end
  end
end