arizona_reloader (arizona v0.4.0)

View Source

Dev-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:

  1. If rebar_agent is registered (running under rebar3 shell), uses rebar_agent:do(compile) so deps and compile options stay aligned with the project.
  2. Otherwise falls back to compile:file/2 with options recovered from the existing module's module_info(compile).
  3. 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_agent compile 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 (reloader env or dev MCP)?
  • broadcast/0 -- emit a reload message manually
  • reload_erl/1 -- compile + reload + broadcast (called by the watcher)
  • sync/0 -- force a compile+reload sync now (called by the dev MCP reload tool)
  • 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

broadcast()

-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.

clear_error()

-spec clear_error() -> ok.

Clears every stashed compile error. Called after a clean whole-project compile.

compile(Files)

-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.

enabled()

-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.

format_error(Reason)

-spec format_error(Reason) -> string() when Reason :: {load_failed, module(), term()}.

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.

get_error()

-spec get_error() -> undefined | map().

Returns the stashed compile errors of every file still failing, or undefined when none is.

join(Pid)

-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).

leave(Pid)

-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.

reload_css()

-spec reload_css() -> ok.

Broadcasts a CSS reload message. No-op when arizona_pubsub is not running (production).

reload_css(Files)

-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.

reload_erl(Files)

-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.

sync()

-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.