defmodule Drab.Waiter do require Logger @moduledoc """ Enables Drab Waiter functionality - synchronous wait for browser events in the Commander handler function. This module is optional and is not loaded by default. You need to explicitly declare it in the commander: use Drab.Commander, modules: [Drab.Waiter] Introduces DSL for registering events. Syntax: waiter(socket) do on "selector1", "event_name", fn (sender) -> end on "selector2", "event_name", fn (sender) -> end on_timeout 5000, fn -> end end """ use DrabModule @impl true def js_templates(), do: ["drab.waiter.js"] @doc """ Main Waiter loop. Takes socket as an argument, returns the return value of the function which matched the selector and event. Inside the `do` block you may register Browser Events which Waiter will react to. See `Drab.Waiter.on`. """ defmacro waiter(socket, do: block) do quote do {:ok, var!(buffer, Drab.Waiter)} = start_buffer(%{:timeout => {:infinity, nil}}) unquote do block end {timeout, timeout_function} = get_buffer(var!(buffer, Drab.Waiter))[:timeout] waiters = buffer |> var!(Drab.Waiter) |> get_buffer() |> Map.delete(:timeout) :ok = stop_buffer(var!(buffer, Drab.Waiter)) with_tokens = Enum.map(waiters, fn {ref, {selector, event_name, function, pid}} -> waiter_token = tokenize_waiter(unquote(socket), pid, ref) %{selector: selector, event_name: event_name, drab_waiter_token: waiter_token} end) Drab.push(unquote(socket), self(), nil, "register_waiters", waiters: with_tokens) ret = receive do {:waiter, ref, sender} -> {selector, event_name, function, pid} = waiters[ref] function.(sender) after timeout -> timeout_function.() end Drab.push(unquote(socket), self(), nil, "unregister_waiters", waiters: with_tokens) ret end end @doc """ Registers Javascript `event_name` on `selector` in the Drab Waiter loop. When the main loop is launched, Drab freezes the current function process and starts waiting for the events. When event occurs, it matches it and runs the corresponding lambda. Example: ret = waiter(socket) do on "#button1", "click", fn(sender) -> sender["text"] end on "#input1", "keyup", fn(sender) -> sender["val"] end end Lambda receives sender: the same Map as the Event Handler does, known there are `dom_sender`. """ defmacro on(selector, event_name, function) do quote bind_quoted: [selector: selector, event_name: event_name, function: function] do put_buffer(var!(buffer, Drab.Waiter), make_ref(), {selector, event_name, function, self()}) end end @doc """ Register timeout event in Drab Waiter loop. Launches anonymous function after given time (in milliseconds). When no timeout is given, Waiter will wait forever. Example: ret = waiter(socket) do on "#button1", "click", fn(sender) -> sender["text"] end on_timeout 5000, fn() -> "timed out" end end """ defmacro on_timeout(timeout, function) do quote bind_quoted: [timeout: timeout, function: function] do put_buffer(var!(buffer, Drab.Waiter), :timeout, {timeout, function}) end end @doc false def start_buffer(state), do: Agent.start_link(fn -> state end) @doc false def stop_buffer(buff), do: Agent.stop(buff) @doc false def put_buffer(buff, key, value), do: Agent.update(buff, fn state -> Map.put(state, key, value) end) @doc false def get_buffer(buff), do: Agent.get(buff, & &1) @doc false @spec tokenize_waiter(Phoenix.Socket.t(), pid, reference) :: String.t() def tokenize_waiter(socket, pid, ref) do Drab.Live.Crypto.encrypt({pid, ref}, Drab.Core.endpoint(socket), "drab_waiter_token") end @doc false @spec detokenize_waiter(Phoenix.Socket.t(), String.t()) :: {pid, reference} def detokenize_waiter(socket, token) do case Drab.Live.Crypto.decrypt(token, Drab.Core.endpoint(socket), "drab_waiter_token") do :error -> raise "Drab.Waiter token mismatch" x -> x end end end