defmodule Forum.Forms.Watcher do @moduledoc """ Watches the canonical forms.html file and reloads `Forum.Forms` when it changes. This intentionally uses a small polling loop instead of an external file watcher dependency so it works the same way in dev, test, and releases. """ use GenServer require Logger @poll_ms 500 @debounce_ms 200 def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @impl true def init(_opts) do paths = paths() state = %{paths: paths, signature: signature(paths), debounce_ref: nil} schedule_poll() {:ok, state} end @impl true def handle_info(:poll, state) do paths = paths() next_signature = signature(paths) state = %{state | paths: paths} state = if next_signature != state.signature do schedule_reload(%{state | signature: next_signature}) else state end schedule_poll() {:noreply, state} end def handle_info({:reload, signature}, %{signature: signature} = state) do case reload_forms() do :ok -> Logger.info("Forum.Forms.Watcher: reloaded #{Enum.join(state.paths, ", ")}") broadcast_reload() {:error, reason} -> Logger.error("Forum.Forms.Watcher: reload failed: #{inspect(reason)}") end {:noreply, %{state | debounce_ref: nil}} end def handle_info({:reload, _stale_signature}, state) do {:noreply, state} end defp schedule_reload(%{debounce_ref: ref} = state) when is_reference(ref) do Process.cancel_timer(ref) schedule_reload(%{state | debounce_ref: nil}) end defp schedule_reload(state) do ref = Process.send_after(self(), {:reload, state.signature}, @debounce_ms) %{state | debounce_ref: ref} end defp schedule_poll do Process.send_after(self(), :poll, @poll_ms) end defp paths do [Forum.Forms.path() | Enum.map(Forum.Forms.network_paths(), fn {_slug, path} -> path end)] end defp signature(paths) do Enum.map(paths, fn path -> case File.read(path) do {:ok, raw} -> {path, :ok, :erlang.phash2(raw), byte_size(raw)} {:error, reason} -> {path, :error, reason} end end) end defp reload_forms do try do Forum.Forms.reload() rescue error -> {:error, error} catch kind, reason -> {:error, {kind, reason}} else :ok -> :ok other -> {:error, other} end end defp broadcast_reload do html = Forum.Forms.html() for {pid, _value} <- Registry.lookup(Forum.FormsSubscriberRegistry, :forms) do send(pid, {:forms_reloaded, html}) end end end