# `Mob.PostMortem.Journal`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/post_mortem/journal.ex#L1)

Keeps what a destructive post-mortem drain returned until someone has
observed it.

Android's `ApplicationExitInfo` drain (`Mob.PostMortem.Android`) advances an
on-disk marker when it returns an exit, and iOS MetricKit
(`Mob.PostMortem.IOS`) hands a payload over once. Either way the only copy
left is the capsule on this boot's in-memory `Mob.Defect.Bus`. If the boot
dies before anyone looks — `mix mob.connect` restarts the app, for one — the
exit is gone from every surface.

So each sweep writes what it drained to `mob_post_mortem_journal.etf` in `Mob.data_dir/0`
**before** emitting it, and re-emits every entry still in the journal on
every sweep, including the first sweep of the next boot. Within one boot,
`Mob.PostMortem.Registry.emit_once/2` keeps a re-sweep from emitting an entry
twice.

## Observed

An entry leaves the journal once its own capsule has been seen:

  * the emit handed it to at least one `Mob.Defect.Bus` subscriber, or
  * `Mob.Defect.Bus.recent/1` returned it.

Nothing else counts. `classes/1` shows a class row, not the occurrence, and
subscribing, or a later capsule reaching a subscriber, says nothing about a
capsule emitted before. A `recent/1` that runs between a sweep's emit and its
bookkeeping is missed, and the entry is emitted again by the next boot: the
journal errs toward a duplicate, never toward a loss.

## Bounded and never fatal

The journal holds at most 32 entries. Past that the oldest are dropped
and counted (`read/1`'s `dropped`), and a warning is logged; a sweep still
emits everything it drained. A journal that cannot be decoded is treated as
empty, logged, and overwritten by the same sweep, so the warning appears
once. Writes go to a temporary file that is synced and renamed over the
journal.

Every read-modify-write of the file runs in one node-local process,
registered as `Mob.PostMortem.Journal`, started on first use and unlinked.
It also holds which of this boot's capsules are still waiting to be
observed; a `:persistent_term` flag, set only while that set is non-empty,
is all `Mob.Defect.Bus.recent/1` reads before deciding to call it. It is
called only by sweeps, and by `recent/1` while entries are waiting; never by
an emit. The file is the record, so the process dying loses no evidence: a
caller it was serving logs and carries on (a sweep still emits), the next
caller starts a new one, and entries whose observation it can no longer
match are emitted again by the next boot. Nothing here raises into a sweep
or a bus reader.

# `entry`

```elixir
@type entry() :: {source :: atom(), id :: String.t(), map()}
```

A drained entry: its source, its artifact id, and the map the NIF returned.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `default_path`

```elixir
@spec default_path() :: Path.t()
```

The journal's path on this device: `mob_post_mortem_journal.etf` in `Mob.data_dir/0`.

# `observed`

```elixir
@spec observed([Mob.Defect.Capsule.t()]) :: :ok
```

Clear the journaled entries whose capsules are among `capsules`.

`Mob.Defect.Bus.recent/1` calls this with what it returns. Unless this boot
emitted journaled entries nobody has observed yet, it is one
`:persistent_term` read. Never raises.

# `read`

```elixir
@spec read(Path.t()) :: %{entries: [entry()], dropped: non_neg_integer()}
```

What the journal at `path` (default: `default_path/0`) holds: the entries
still waiting to be observed, oldest first, and how many were ever dropped
past the 32-entry bound.

# `sweep`

```elixir
@spec sweep((-&gt; Path.t()), atom(), [{String.t(), map()}], (map() -&gt;
                                                       Mob.Defect.Capsule.t())) ::
  [
    Mob.Defect.Capsule.t()
  ]
```

Journal `fresh` (`{id, entry}` pairs drained from `source`), then emit every
entry of `source` still in the journal followed by the fresh ones, each
through `Registry.emit_once/2` as the capsule `build` makes of it.

An emitted capsule that reached a subscriber is cleared at once; the rest
wait for `observed/1`. `path` is resolved lazily, so a data directory that
cannot be resolved costs the journal, not the sweep. Returns the capsules
emitted.

---

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