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

The only process permitted to call the render NIFs.

## Why this is forced

Not a style choice — the native tap registry requires it. From
`ios/mob_nif.m` (the Android side in `android/jni/mob_nif.zig` is the same
shape):

    static TapHandle *tap_tables[2];   // grown on demand, see MOB-133
    static int tap_active = 0;
    static int tap_build_count = 0;   // cursor into the BUILDING table

`clear_taps` prepares the inactive table and resets the cursor, `register_tap`
appends at `tap_build_count++`, and `set_root` swaps the tables atomically.
The double buffering makes a *concurrent reader* safe — a drag or scroll event
arriving mid-render still resolves against the last committed table. It does
nothing for concurrent *writers*: there is one global build cursor, so two
renders in flight interleave their handles into the same building table, and
whichever reaches `set_root` first commits a table holding both screens'
handles while the other screen's tree is never committed at all.

So `clear_taps -> register_tap* -> set_root` is one indivisible sequence, and
serialising it through a single process is the only thing that keeps it that
way once more than one screen is live (MOB-112).

## Coalescing falls out of it

Because renders are queued rather than executed by the caller, the sender can
look at what is waiting and commit only what matters:

* for a given screen, only the newest tree is committed — a screen that
  re-renders three times before the sender gets to it produces one commit, not
  three
* a tree for a screen that is not active is dropped, never committed

That second point is what lets an inactive tab keep its state without
rendering. It is also why switching stacks re-renders: the incoming screen's
tree is produced fresh at switch time rather than replayed from a queue.

## Ordering

`render/5` is asynchronous, so a caller that needs the commit to have landed
calls `sync/1`, which performs the flush itself rather than waiting for the
self-sent one.

It has to. `send(self(), :flush)` during the render cast appends to the *back*
of the mailbox — behind a `sync/1` the caller has already queued — so a
`sync/1` that merely replied would return before the frame was committed.
Mailbox order is the wrong tool here, and it looks like the right one.

`Mob.Router` uses an activation-frame token before asking a screen to paint.
Activation is synchronous and carries the navigation transition; only the
router-requested paint bearing that token may cross the boundary. A timer
repaint that began while the screen was parked is therefore dropped even if
its cast reaches the sender after activation.

`Mob.Router` uses `sync/1` on its `handle_call` paths to keep the guarantee
`Mob.Test` documents for the synchronous navigation helpers. Note the ordering
guarantee only covers renders cast by the *calling* process; the BEAM promises
nothing about the relative order of sends from different processes.

# `screen_ref`

```elixir
@type screen_ref() :: reference() | atom()
```

Identifies which screen a tree belongs to — one per live screen since
MOB-112, not one per navigation stack. Screens below the top of a stack are
live processes that repaint, so a stack-wide key would let a background
screen's tree commit over the foreground one.

# `activate`

```elixir
@spec activate(screen_ref(), atom()) :: :ok
```

Activate a screen and reserve its navigation transition for the next frame.

Unlike `set_active/1`, this call is synchronous. The router uses it at the
navigation boundary so paints sent by different screen processes cannot be
observed before the transition intent. The first tree for `ref` consumes the
reservation; later ordinary repaints remain `:none`.

A `:none` transition only activates the screen and creates no reservation.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `ensure_started`

```elixir
@spec ensure_started() :: :ok
```

Start the sender if it is not already running.

`Mob.App.start/0` starts it on the normal boot path, but a screen can be
started without going through `Mob.App` — `liveview_notes.md` documents
exactly that — and a missing sender fails in the worst possible way: renders
are casts, so they vanish silently and the app shows a blank screen with no
log, until the first synchronous render exits `:noproc`. `Mob.Router` calls
this so no render path can reach that state.

Deliberately unlinked. The caller is usually a screen, and a screen crash must
not take down the process every other screen renders through.

# `render`

```elixir
@spec render(screen_ref(), map(), atom(), module() | atom(), atom()) :: :ok
```

Queue `tree` for commit on behalf of screen `ref`.

Returns immediately. The tree is committed only if `ref` is active when the
sender gets to it, and only if no newer tree for `ref` has arrived by then.

# `running?`

```elixir
@spec running?() :: boolean()
```

Whether the sender is running. Renders are silently dropped when it is not.

# `set_active`

```elixir
@spec set_active(screen_ref()) :: :ok
```

Declare which screen's trees may be committed.

A render for any other screen is dropped. `Mob.Router` sets this;
MOB-113's router takes it over.

# `start_link`

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

Start the sender. Named, so there is exactly one.

# `sync`

```elixir
@spec sync(timeout()) :: :ok
```

Block until every render queued before this call has been committed or
dropped.

"Queued before" means cast by the *calling* process — the BEAM orders sends
between a given pair of processes and says nothing about sends from different
ones. Committed *or dropped*: a return of `:ok` does not promise the caller's
own tree reached the screen, only that the sender has caught up. A tree for a
screen that is not active is dropped, and `sync/1` returns `:ok` all the same.

---

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