# `Mead.Session.Store`
[🔗](https://github.com/albertosca/mead/blob/main/lib/mead/session/store.ex#L1)

Keeps the turns of each conversation in one append-only file per session.

A file is a sequence of records framed by `Mead.Session.Record`, each the encoding of one event: `{:append, turn}` when a turn starts, `{:replace_last, turn}` when the newest turn is closed, and `:interrupt` when every turn still in progress became interrupted. Replaying the events in order rebuilds the list the history agent held.

A last record that is cut short or fails its payload checksum is what a crash during a write
leaves, so it is dropped with a warning. The length has a checksum of its own, so a flipped bit
in a header cannot pass for a cut write: damage anywhere else is `:corrupt_history`, because a
conversation with a hole in the middle is worse than no conversation. `load/2` only reads;
`recover/2` also cuts the dropped tail off the file, which a session must do before it writes
again, or its next record would land behind the garbage and make the whole file unreadable. A
write that fails is rolled back to the size the file had before it, for the same reason. The
directory is single-writer.

`compact/3` rewrites a file as one `{:append, turn}` per turn, through a synced `<id>.log.tmp`
renamed over the log, so a crash leaves the old file or the new one and `load/2` answers the
same turns before and after.

Payloads are decoded without `:safe`. A failed turn keeps its reason as output, and that reason
can hold an exception or an atom from a module a freshly booted node has not loaded yet, which
safe decoding refuses: a conversation with a failure in it could not be resumed. The files are
written by this node, are private to its owner, and are guarded by a checksum; decoding never
runs code, and whoever can write the directory can already do far worse. Do not point the
directory at data from a source you do not trust.

A session id becomes a file name, so it is accepted only in the shape `Mead.UUID.generate/0`
produces, and checked before any path is built.

# `event`

```elixir
@type event() ::
  {:append, Mead.Session.Turn.t()}
  | {:replace_last, Mead.Session.Turn.t()}
  | :interrupt
```

# `append`

```elixir
@spec append(String.t(), String.t(), event()) :: :ok | {:error, term()}
```

Appends one event to the file of a session, creating the directory and file on first use.

# `compact`

```elixir
@spec compact(String.t(), String.t(), [Mead.Session.Turn.t()]) ::
  {:ok, %{bytes_before: non_neg_integer(), bytes_after: non_neg_integer()}}
  | {:error, :not_found | {:persistence, term()}}
```

Rewrites the file of a session as one `{:append, turn}` record per turn, given in
chronological order, and returns the size of the file before and after.

The file is replaced only when the new form is smaller, so a file that is already compact is
left alone and both sizes are the current one. The replacement is written to `<id>.log.tmp`,
synced, and renamed over the log, so a crash leaves the old file or the new one. A failure
leaves the log as it was and removes the temporary file. A file that does not exist is
`{:error, :not_found}`: compaction never creates a conversation.

# `compact_after_bytes`

```elixir
@spec compact_after_bytes() :: pos_integer()
```

Returns the size in bytes a live session's file may reach before it is compacted.

Defaults to one mebibyte; `config :mead, :storage, compact_after_bytes:` sets it. Anything but
a positive integer raises `ArgumentError`, rather than quietly never compacting.

# `delete`

```elixir
@spec delete(String.t(), String.t()) :: :ok | {:error, term()}
```

Removes the file of a session, and the temporary file a crashed compaction may have left beside
it; a missing file or an invalid id is not an error.

# `directory`

```elixir
@spec directory() :: String.t() | nil
```

Returns the configured storage directory, or `nil` when persistence is off.

# `exists?`

```elixir
@spec exists?(String.t(), String.t()) :: boolean()
```

Returns true when a file exists for the session.

# `ledger_journal_max_bytes`

```elixir
@spec ledger_journal_max_bytes() :: pos_integer()
```

Returns the size in bytes the ledger journal may reach before it is rewritten as a checkpoint.

Defaults to one mebibyte; `config :mead, :storage, ledger_journal_max_bytes:` sets it, and `nil` means the default. Anything but a positive integer raises `ArgumentError`.

# `list`

```elixir
@spec list(String.t()) ::
  {:ok, [%{session_id: String.t(), updated_at: DateTime.t()}]}
  | {:error, {:persistence, term()}}
```

Lists the stored sessions, newest first, as maps with the `:session_id` and the `:updated_at` of
its file.

Only names and file times are read, never the content, so the cost does not grow with the
length of the conversations. Anything in the directory that is not a session file is ignored,
and a damaged file is listed like any other: only `recover/2` reads it. A directory that does
not exist yet holds no sessions.

# `load`

```elixir
@spec load(String.t(), String.t()) ::
  {:ok, [Mead.Session.Turn.t()]}
  | {:error, :not_found | :corrupt_history | {:persistence, term()}}
```

Returns the turns of a session in chronological order, without touching the file.

# `max_turns`

```elixir
@spec max_turns() :: pos_integer() | nil
```

Returns how many of the newest turns a conversation keeps when its file is compacted, or `nil`
for all of them, which is the default.

`config :mead, :storage, max_turns:` sets it. It is the one setting that makes compaction drop
turns, so it is off unless asked for; anything but `nil` or a positive integer raises
`ArgumentError`.

# `recover`

```elixir
@spec recover(String.t(), String.t()) ::
  {:ok, [Mead.Session.Turn.t()]}
  | {:error, :not_found | :corrupt_history | {:persistence, term()}}
```

Like `load/2`, and cuts a torn last record off the file so that later appends follow the last
complete record.

# `size`

```elixir
@spec size(String.t(), String.t()) ::
  {:ok, non_neg_integer()} | {:error, :not_found | {:persistence, term()}}
```

Returns the size in bytes of the file of a session.

# `window`

```elixir
@spec window([Mead.Session.Turn.t()], pos_integer() | nil) :: [Mead.Session.Turn.t()]
```

The newest `maximum` of `turns` (chronological order), or all of them when it is `nil`.

---

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