arizona_reloader (arizona v0.4.0)
View SourceDev-mode hot reloader for .erl and CSS files.
Subscribers join/1 the reloader's pubsub topic and receive
{arizona_reloader, reload} (or reload_css) messages whenever files
change: the browser's SSE reload endpoint, and -- while the reloader is
enabled/0 -- every live view's transport, which remounts the view on
fresh code (a WebSocket closes 1001 so its client reconnects; a
?terminal session remounts in place). A reloader rule process
(arizona_reloader_rule, fed by the configured arizona_watcher
implementation) calls reload_erl/1 or reload_css/1 after a debounced
burst of file changes.
Compilation flow
reload_erl/1 recompiles changed .erl files, then triggers a route
recompile and broadcasts a reload message:
- If
rebar_agentis registered (running underrebar3 shell), usesrebar_agent:do(compile)so deps and compile options stay aligned with the project. - Otherwise falls back to
compile:file/2with options recovered from the existing module'smodule_info(compile). - On compile error, structured errors from the changed files are
stashed in a persistent term, per file, so the dev error page can
render them on the next request. A file's errors stay until a wave
that carries it compiles it (or drops it as deleted); a clean
rebar_agentcompile builds the whole project, so it clears them all.
After the compile, a sweep reloads any module whose loaded code no longer
matches its beam on disk. Apps listed in rebar3's app_reload_blacklist shell
option (or the rebar refresh_paths_blacklist env that overrides it) are
skipped: reloading a module purges the old code, which kills any process still
lingering in it -- a listener blocked in its own loop, typically -- so every
recompile would otherwise log a supervisor report.
Public reload API
join/1-- subscribe a pid to the reloader topic (idempotent)leave/1-- unsubscribe a pid from it (idempotent)enabled/0-- can this node broadcast a code reload (reloaderenv or dev MCP)?broadcast/0-- emit a reload message manuallyreload_erl/1-- compile + reload + broadcast (called by the watcher)sync/0-- force a compile+reload sync now (called by the dev MCPreloadtool)reload_css/0,1-- broadcast a CSS reload (no compile needed)get_error/0/clear_error/0-- inspect/reset the last compile error
Summary
Functions
Broadcasts an {arizona_reloader, reload} message on the reloader
topic. The SSE endpoint relays it to the browser, which reloads the page;
a subscribed live view transport remounts its view.
Clears every stashed compile error. Called after a clean whole-project compile.
Compiles a list of source files. Filters to .erl files, then either
delegates to rebar_agent (when running under rebar3 shell) or
falls back to a direct compile:file/2 per file.
Whether this node can broadcast a code reload: the reloader app env holds
enabled => true and a rules list -- an empty one too, which starts no
arizona_reloader_rule but leaves the node able to reload (something else
calls reload_erl/1 or broadcast/0) -- or an arizona_dev_mcp route is
mounted, whose reload tool runs sync/0 with or without rule processes.
Formats a diagnostic this module adds to the stashed compile errors, the way
erl_lint:format_error/1 formats its own: the dev error page calls it through
the {Location, Module, Reason} triple.
Returns the stashed compile errors of every file still failing, or
undefined when none is.
Subscribes Pid to the reloader pubsub topic. Idempotent: re-joining
returns ok.
Unsubscribes Pid from the reloader pubsub topic. Idempotent: leaving
when not subscribed returns ok. No-ops, like join/1, if
arizona_pubsub is not running.
Broadcasts a CSS reload message. No-op when arizona_pubsub is not
running (production).
Triggers a CSS reload broadcast. Ignores the file list -- the client re-fetches its stylesheet on receipt.
Recompiles the changed .erl files, refreshes routes, and
broadcasts a reload message. Called by the file watcher.
Forces a compile+reload sync now, without waiting for a watcher event: under
rebar3 shell the project is recompiled through rebar_agent (which also
hot-reloads the modules it rebuilds); then any module whose loaded code still
differs from its beam on disk (a rebuild outside the watcher's eye) is
reloaded from that beam. Refreshes routes and broadcasts a reload on success,
mirroring reload_erl/1. Called by the dev MCP's reload tool.
Functions
-spec broadcast() -> ok.
Broadcasts an {arizona_reloader, reload} message on the reloader
topic. The SSE endpoint relays it to the browser, which reloads the page;
a subscribed live view transport remounts its view.
-spec clear_error() -> ok.
Clears every stashed compile error. Called after a clean whole-project compile.
-spec compile(Files) -> ok | {error, map()} when Files :: [file:filename()].
Compiles a list of source files. Filters to .erl files, then either
delegates to rebar_agent (when running under rebar3 shell) or
falls back to a direct compile:file/2 per file.
Returns ok when this wave compiled, or {error, #{errors := [...]}} with
this wave's errors. The stash get_error/0 reads is kept per file, so it can
still hold another file's errors after an ok: a broken file stays reported
until a wave compiles it, not until any unrelated file compiles.
-spec enabled() -> boolean().
Whether this node can broadcast a code reload: the reloader app env holds
enabled => true and a rules list -- an empty one too, which starts no
arizona_reloader_rule but leaves the node able to reload (something else
calls reload_erl/1 or broadcast/0) -- or an arizona_dev_mcp route is
mounted, whose reload tool runs sync/0 with or without rule processes.
A live view's transport joins the reload topic only when this holds, so a production node pays one persistent-term read and one app-env read per connection, and nothing else.
Formats a diagnostic this module adds to the stashed compile errors, the way
erl_lint:format_error/1 formats its own: the dev error page calls it through
the {Location, Module, Reason} triple.
-spec get_error() -> undefined | map().
Returns the stashed compile errors of every file still failing, or
undefined when none is.
-spec join(Pid) -> ok when Pid :: pid().
Subscribes Pid to the reloader pubsub topic. Idempotent: re-joining
returns ok.
Silently no-ops if arizona_pubsub is not running (production mode).
-spec leave(Pid) -> ok when Pid :: pid().
Unsubscribes Pid from the reloader pubsub topic. Idempotent: leaving
when not subscribed returns ok. No-ops, like join/1, if
arizona_pubsub is not running.
-spec reload_css() -> ok.
Broadcasts a CSS reload message. No-op when arizona_pubsub is not
running (production).
-spec reload_css(Files) -> ok when Files :: [file:filename()].
Triggers a CSS reload broadcast. Ignores the file list -- the client re-fetches its stylesheet on receipt.
-spec reload_erl(Files) -> ok when Files :: [file:filename()].
Recompiles the changed .erl files, refreshes routes, and
broadcasts a reload message. Called by the file watcher.
After the reload is broadcast, runs a best-effort call-consistency check
(arizona_reloader_consistency:check/1) that warns about a loaded module left
calling a function the just-reloaded module no longer exports (the classic
stale-beam-after-mtime-skip mismatch that otherwise stays silent until a runtime
undef). It is advisory, so it runs after the broadcast -- never in the
reload's critical path, and its beam sweep can never delay the client seeing the
change. It never crashes or interferes with the reload.
-spec sync() -> {ok, Info} | {error, term()} when Info :: #{agent := boolean(), reloaded := [module()]}.
Forces a compile+reload sync now, without waiting for a watcher event: under
rebar3 shell the project is recompiled through rebar_agent (which also
hot-reloads the modules it rebuilds); then any module whose loaded code still
differs from its beam on disk (a rebuild outside the watcher's eye) is
reloaded from that beam. Refreshes routes and broadcasts a reload on success,
mirroring reload_erl/1. Called by the dev MCP's reload tool.
Returns {ok, #{agent := AgentUsed, reloaded := Mods}} on success, or
{error, Reason} when the rebar_agent compile fails. The failure is
reported to the caller only -- the watcher wave owns the dev error page's
stash, and a forced sync has no changed-file list to collect structured
errors from.