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