init
This commit is contained in:
108
lib/forum/forms/watcher.ex
Normal file
108
lib/forum/forms/watcher.ex
Normal file
@@ -0,0 +1,108 @@
|
||||
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
|
||||
Reference in New Issue
Block a user