Packages
mob_dev
0.6.11
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/server/watch_worker.ex
defmodule MobDev.Server.WatchWorker do
@moduledoc """
GenServer that runs the mob.watch loop inside the mob.server process.
Polls `lib/**/*.ex` for changes every 500ms. When files change it
debounces, recompiles, and hot-pushes changed modules to all connected
device nodes via Erlang dist — same logic as `mix mob.watch` but driven
from the dashboard UI instead of a terminal.
Events broadcast to the `"watch"` PubSub topic:
{:watch_status, :watching | :idle}
{:watch_push, %{pushed: n, failed: [...], nodes: [...], files: [...]}}
"""
use GenServer
require Logger
alias MobDev.HotPush
@pubsub MobDev.PubSub
@topic "watch"
@cookie :mob_secret
# ms between source polls
@interval 500
# ms to wait after first change before compiling
@debounce 300
# ── Public API ──────────────────────────────────────────────────────────────
@spec start_link(term()) :: GenServer.on_start()
def start_link(_), do: GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
@doc "Start watching. Idempotent — safe to call when already watching."
@spec start_watching() :: term()
def start_watching, do: GenServer.call(__MODULE__, :start)
@doc "Stop watching."
@spec stop_watching() :: term()
def stop_watching, do: GenServer.call(__MODULE__, :stop)
@doc "Returns %{watching: bool, nodes: [node()], last_push: map | nil}."
@spec status() :: map()
def status, do: GenServer.call(__MODULE__, :status)
@doc "Subscribe the calling process to watch PubSub events."
@spec subscribe() :: :ok | {:error, term()}
def subscribe, do: Phoenix.PubSub.subscribe(@pubsub, @topic)
# ── GenServer ────────────────────────────────────────────────────────────────
@impl GenServer
def init(:ok) do
{:ok, %{watching: false, sources: %{}, nodes: [], timer: nil, last_push: nil}}
end
@impl GenServer
def handle_call(:start, _from, %{watching: true} = state) do
{:reply, :already_watching, state}
end
def handle_call(:start, _from, state) do
nodes = connect_nodes()
sources = MobDev.SourceWatch.snapshot()
timer = schedule_tick()
state = %{state | watching: true, sources: sources, nodes: nodes, timer: timer}
broadcast({:watch_status, :watching})
Logger.info("WatchWorker: started, #{length(nodes)} node(s) connected")
{:reply, :ok, state}
end
def handle_call(:stop, _from, state) do
if state.timer, do: Process.cancel_timer(state.timer)
broadcast({:watch_status, :idle})
Logger.info("WatchWorker: stopped")
{:reply, :ok, %{state | watching: false, nodes: [], timer: nil}}
end
def handle_call(:status, _from, state) do
{:reply, %{watching: state.watching, nodes: state.nodes, last_push: state.last_push}, state}
end
@impl GenServer
def handle_info(:tick, %{watching: false} = state), do: {:noreply, state}
def handle_info(:tick, state) do
current = MobDev.SourceWatch.snapshot()
changed = MobDev.SourceWatch.diff(state.sources, current)
state =
if changed == [] do
state
else
# Debounce — let format-on-save and multi-file saves settle.
Process.sleep(@debounce)
current2 = MobDev.SourceWatch.snapshot()
nodes = reconnect(state.nodes)
if nodes == [] do
Logger.warning("WatchWorker: files changed but no nodes connected — skipping push")
%{state | sources: current2, nodes: nodes}
else
snapshot = HotPush.snapshot_beams()
compile()
{pushed, failed} = HotPush.push_changed(nodes, snapshot)
push_info = %{
pushed: pushed,
failed: failed,
nodes: nodes,
files: Enum.map(changed, &Path.relative_to_cwd/1),
at: DateTime.utc_now()
}
if pushed > 0 or failed != [] do
broadcast({:watch_push, push_info})
Logger.info("WatchWorker: pushed #{pushed} module(s) to #{length(nodes)} node(s)")
end
%{state | sources: current2, nodes: nodes, last_push: push_info}
end
end
{:noreply, %{state | timer: schedule_tick()}}
end
# ── Helpers ──────────────────────────────────────────────────────────────────
defp schedule_tick, do: Process.send_after(self(), :tick, @interval)
defp connect_nodes do
try do
HotPush.connect(cookie: @cookie)
rescue
_ -> []
end
end
defp reconnect(nodes) do
alive = Enum.filter(nodes, &(Node.connect(&1) == true))
new = connect_nodes()
Enum.uniq(alive ++ new)
end
defp compile do
mix = System.find_executable("mix") || "mix"
System.cmd(mix, ["compile"], cd: File.cwd!(), stderr_to_stdout: true)
end
defp broadcast(msg), do: Phoenix.PubSub.broadcast(@pubsub, @topic, msg)
end