This commit is contained in:
Sam
2026-06-10 11:51:56 -05:00
commit 66ba338b81
57 changed files with 5509 additions and 0 deletions

108
lib/forum/forms/watcher.ex Normal file
View 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