# `Mob.Diag.Store`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/diag/store.ex#L1)

The owner and lifecycle every diagnostic store shares.

`Mob.Agent.Receipts`, `Mob.Defect.Bus`, `Mob.Invariant`,
`Mob.PostMortem.Registry` and `Mob.RenderStats` keep what they record in
public named ETS tables that callers write directly, with no mailbox on the
hot path. A table dies with the process that created it, so each needs an
owner that outlives its writers. They used to carry five hand-written copies
of that owner, and all of them had the same hole: `GenServer.start/3`
registers the name before `init/1` runs, so a second concurrent first caller
got `{:error, {:already_started, pid}}` and wrote to a table that did not
exist yet. Measured: up to 1,400 of 1,600 concurrent first calls saw no table.

This module is that owner, once, with the properties a diagnostic needs:

  * **Ready means ready.** `ensure/1` returns only after the store's tables
    exist. The hot path is a single `:ets.whereis/1` on the store's flag table,
    which is created last; only a miss pays for a call to the owner, and that
    call queues behind `init/1`.
  * **Evidence outlives its owner.** Tables are created with
    `Mob.Diag.Heir` as their ETS heir. If an owner dies, its tables and rows
    pass to the heir and stay writable, and the heir starts the next owner,
    which takes them back. The owner re-points the heir if the heir itself
    restarts.
  * **Setup is idempotent.** It creates only what is missing, and a store's
    state (counters, sequence numbers) is carried into the new state, so
    `reload/1` re-reads configuration without resetting counters.
  * **State is versioned, and so is this module.** Code loaded by a hot push
    can change a store's state shape, or the machinery here. `state/1`
    compares the stored entry with both the store's `state_vsn/0` and this
    module's own version, and re-runs setup when either differs. So the
    first write after `mix mob.push` gets state it can read, and the first
    write after a `mob` upgrade restarts an owner that older code left down
    (an older heir did not restart owners). Every write path therefore reads
    `state/1`, even one with no state to use.
  * **Writes never raise and never lose silently.** `guard/3` catches any
    failure on a write path, counts it (`lost`), repairs missing tables, and
    returns a fallback. `health/1` reports `lost`, `resets` (tables recreated
    after being lost) and `owner_starts`.

A store module implements the callbacks below. The owner is registered as
`Mob.Diag.Owner.<Store>`, started on first use, and unlinked: `mob` has no
supervision tree of its own, and a diagnostic must neither die with a writer
nor take one down. The name is deliberately not the `<Store>.Owner` the
hand-written owners used: an app that hot-pushes this over an older `mob`
still has those processes running, and calling into one would crash. Here
their tables are simply held by `:other` and written to until the app
restarts.

# `after_setup`
*optional* 

```elixir
@callback after_setup() :: :ok
```

Runs in the owner after every setup, once all tables exist.

# `health`
*optional* 

```elixir
@callback health(state :: map()) :: map()
```

Store-specific, value-free fields for `health/1`.

# `new_state`

```elixir
@callback new_state(previous :: map() | nil) :: map()
```

The store's state, given what was there before (`nil` the first time).

Must carry counters over from `previous` rather than replacing them, and must
tolerate a `previous` of an older shape (after a hot push).

# `state_vsn`

```elixir
@callback state_vsn() :: pos_integer()
```

Version of the map `new_state/1` returns. Bump it when that shape changes.

# `tables`

```elixir
@callback tables() :: [{atom(), list()}]
```

The store's tables, in creation order. The **last** is the flag `ensure/1` checks.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `ensure`

```elixir
@spec ensure(module()) :: :ok
```

Return once `store`'s tables exist.

# `guard`

```elixir
@spec guard(module(), term(), (-&gt; term())) :: term()
```

Run a write path. Any failure is followed by a repair of missing tables,
counted as lost and logged the first time; `fallback` is returned instead.

# `health`

```elixir
@spec health(module()) :: map()
```

Value-free health of `store`. Read-only: never starts an owner or creates a
table, so it reports a broken store instead of repairing it. Never raises:
state left in an older shape by a hot push, which the store's `health/1`
cannot read, is reported as `store: :stale` until the next write updates it.
A `framework_vsn` that differs from the expected one, after a `mob` upgrade,
leaves the store's state readable; the next write sets the store up again.

# `note_lost`

```elixir
@spec note_lost(module(), atom(), term()) :: :ok
```

Count one write lost by `store`, outside `guard/3`. Missing tables or a
missing state are set up first, so a loss before the store's first setup,
or after a hot push onto tables an older `mob` still holds, is counted too.

# `reload`

```elixir
@spec reload(module()) :: :ok
```

Re-run setup: create anything missing and re-read configuration, keeping counters.

# `state`

```elixir
@spec state(module()) :: map()
```

The store's current state, re-running setup first if it was set up by
another version of the store or of this module.

---

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