# `Mob.Router`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/router.ex#L1)

Owns navigation, and the one process per live screen that serves it.

Which stacks exist, which is active, and one `Mob.Screen.Server` per live
screen — this process starts them, stops them, and restarts one that crashes.
It keeps the `:mob_screen` registered name, so the native layer's
`enif_whereis_pid` lookups (back gesture, alert actions, notifications) are
unaffected.

## Not in the per-message path

A screen handling an ordinary message never touches this process. Native
events reach a screen directly: `Mob.Listener` unwraps the envelope and sends
to the screen's own pid, the screen renders, and `Mob.Sender` commits. The
router hears only about navigation.

That is the property MOB-113 exists to guarantee, and it is what makes one
process per screen affordable. An earlier costing of this design assumed a
router in the loop and concluded per-screen processes could not escape a hop
per message; splitting the router from the sender is what dissolved that.

`Mob.Screen` delegates its public API here, so callers keep using
`Mob.Screen.dispatch/3` and friends.

## A navigation entry

`%{module:, pid:, params:, ref:}`.

`params` is carried because a restart has to reproduce the screen exactly —
one that mounts on `%{id: id}` cannot come back from `%{}`.

`ref` identifies the screen to `Mob.Sender`, and is unique **per screen**,
not per stack. Every screen is a live process that repaints on any message it
receives, including the ones below the top of a stack; keyed by stack, a timer
tick in a screen the user cannot see would commit its tree — tap table
included — over the screen they can. The sender only commits the tree whose
ref is active. The ref survives a restart, because the replacement is the same
logical screen.

See `decisions/2026-08-28-screen-processes-and-supervision.md`.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `dispatch`

```elixir
@spec dispatch(pid(), String.t(), map()) :: :ok
```

Dispatch a UI event to the screen process. Returns `:ok` synchronously once
the event has been processed and the state updated.

# `entries`

```elixir
@spec entries(pid()) :: [{module(), pid()}]
```

Every screen entry the router holds, as `{module, pid}` — current, history and
parked tabs alike.

For `Mob.Invariant`: `get_nav_history/1` returns sockets, which is what a test
wants, but an invariant needs the pids in order to ask whether they are still
alive.

# `get_current_module`

```elixir
@spec get_current_module(pid()) :: module()
```

Return the module of the currently active screen in the navigation stack.
Intended for testing and debugging.

# `get_nav_history`

```elixir
@spec get_nav_history(pid()) :: [{module(), Mob.Socket.t() | nil}]
```

Return the navigation history (list of `{module, socket}` pairs, head = most recent).
Intended for testing and debugging.

# `get_screen_pid`

```elixir
@spec get_screen_pid(GenServer.server()) :: pid()
```

Return the pid of the process owning the currently active screen.

Each live screen is its own process since MOB-112; this is how tooling
reaches the one that is on screen.

# `get_socket`

```elixir
@spec get_socket(pid()) :: Mob.Socket.t() | nil
```

Return the current socket state of a running screen, or `nil` while that
screen is being restarted.

Intended for testing and debugging — not for production app logic.

# `start_link`

```elixir
@spec start_link(module(), map(), keyword()) :: GenServer.on_start()
```

Start a screen process linked to the calling process.

`params` is passed as the first argument to `mount/3`.

# `start_root`

```elixir
@spec start_root(module(), map(), keyword()) :: GenServer.on_start()
```

Start a screen as the root UI screen. Calls mount, renders the component tree
via `Mob.Renderer`, and calls `set_root` on the resulting view.

This is the main entry point for production use. `start_link/2` is for tests
(no NIF calls).

## When no screen is left

If the root screen fails to start (its `mount/3` returns an error or
raises) while no other router is live, or the current screen crashes,
can't be restarted and there is no other live screen to fall back to, the
app would sit on a blank screen for good: the process stays alive, so
relaunching from the launcher brings the same blank process back. On a
device (the real `:mob_nif`), the router logs the reason, flushes the
logger and ends the process with `System.halt(1)`, so the next launch boots
fresh. That also lets a plugin that watches for a launch that never
becomes stable (mob_deliver's probation) see this one die.

`:on_no_live_screen` replaces that behaviour with a 1-arity function called
with the module of the screen that failed. Anything injected as `:nif`
(host tests) gets no action by default, so a test VM is never halted.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
