# `ExWapp.Store`
[🔗](https://github.com/elchemista/ex_wapp/blob/v0.1.5/lib/ex_wapp/store.ex#L1)

Persistence adapter behaviour for WhatsApp session state.

Adapters implement this behaviour to persist session data including:
- Noise protocol keys (static keypair, handshake hash/salt)
- Signal protocol state (identity, signed prekeys, sessions)
- Client configuration and pairing data
- App state sync data:
  - `:app_state_keys`
  - `:app_state_versions`
  - `:app_state_mutation_macs`
  - `:contacts`

## Built-in Adapters

  * `ExWapp.Store.Ets` - ETS table with file persistence (default)
  * `ExWapp.Store.Memory` - In-memory only, useful for testing

## Passing a Store to Session

```elixir
# Use default ETS adapter
ExWapp.connect(store: nil)

# Use ETS with custom path
ExWapp.connect(store: {ExWapp.Store.Ets, path: "/custom/path.etf"})

# Use memory adapter for testing
ExWapp.connect(store: ExWapp.Store.Memory)

# Use pre-created store instance
store = ExWapp.Store.Ets.new(path: "my_session.etf")
ExWapp.connect(store: store)
```

## Creating a Custom Adapter

Implement the `ExWapp.Store` behaviour:

```elixir
defmodule MyApp.PostgresStore do
  @behaviour ExWapp.Store

  defstruct [:conn, :user_id]

  @impl true
  def new(opts) do
    conn = Keyword.fetch!(opts, :conn)
    user_id = Keyword.fetch!(opts, :user_id)
    %__MODULE__{conn: conn, user_id: user_id}
  end

  @impl true
  def load(%__MODULE__{conn: conn, user_id: user_id}) do
    case MyApp.Repo.get_by(WaSession, user_id: user_id) do
      nil -> {:ok, %{}}
      session -> {:ok, session.data}
    end
  end

  @impl true
  def persist(%__MODULE__{conn: conn, user_id: user_id}, data) do
    MyApp.Repo.insert_or_update!(%WaSession{user_id: user_id, data: data})
    :ok
  end
end
```

# `data`

```elixir
@type data() :: map()
```

# `t`

```elixir
@type t() :: struct()
```

# `begin_batch`
*optional* 

```elixir
@callback begin_batch(store :: t(), reason :: term()) :: :ok | {:error, term()}
```

Begins a caller-owned persistence batch.

# `clear`
*optional* 

```elixir
@callback clear(store :: t()) :: :ok | {:error, term()}
```

Optional callback to clear all stored data.

Useful for testing or when unlinking a device.

# `close`
*optional* 

```elixir
@callback close(store :: t()) :: :ok | {:error, term()}
```

Optional callback that releases adapter-owned processes or connections.

Sessions call this only for store instances they created themselves. A
pre-created store passed as a struct remains owned by the caller.

# `delete_chat_messages`
*optional* 

```elixir
@callback delete_chat_messages(store :: t(), jid :: String.t()) :: :ok | {:error, term()}
```

Deletes every message associated with one chat.

# `delete_messages`
*optional* 

```elixir
@callback delete_messages(store :: t(), jid :: String.t(), message_ids :: [String.t()]) ::
  :ok | {:error, term()}
```

Deletes selected messages after durable application persistence.

# `find_message`
*optional* 

```elixir
@callback find_message(store :: t(), jid :: String.t(), message_id :: String.t()) ::
  map() | nil
```

Finds one message in a chat by ID.

# `find_message_by_id`
*optional* 

```elixir
@callback find_message_by_id(store :: t(), message_id :: String.t()) ::
  {String.t(), map()} | nil
```

Finds one message by ID across all chats.

# `finish_batch`
*optional* 

```elixir
@callback finish_batch(store :: t(), reason :: term()) :: :ok | {:error, term()}
```

Ends a caller-owned persistence batch and durably flushes pending writes.

# `flush`
*optional* 

```elixir
@callback flush(store :: t()) :: :ok | {:error, term()}
```

Durably flushes all pending writes without closing the adapter.

# `get_and_update`
*optional* 

```elixir
@callback get_and_update(store :: t(), key :: atom(), default :: term(), (term() -&gt;
                                                                  {term(),
                                                                   term()})) ::
  term() | {:error, term()}
```

Atomically updates one domain and returns a value from the update.

# `load`

```elixir
@callback load(store :: t()) :: {:ok, data()} | {:error, term()}
```

Loads persisted session data.

Returns `{:ok, data}` with the stored map, or `{:ok, %{}}` if no data exists.
Returns `{:error, reason}` if the load operation fails.

# `new`

```elixir
@callback new(opts :: keyword()) :: t()
```

Creates a new store instance.

Options are adapter-specific. See the adapter module documentation.

# `persist`

```elixir
@callback persist(store :: t(), data :: data()) :: :ok | {:error, term()}
```

Persists session data.

The `data` map contains all session state that needs to survive restarts.
Returns `:ok` on success or `{:error, reason}` on failure.

# `read`
*optional* 

```elixir
@callback read(store :: t(), (data() -&gt; term())) :: term()
```

Reads a consistent snapshot of non-specialized top-level store data.

# `snapshot_messages`
*optional* 

```elixir
@callback snapshot_messages(store :: t()) :: %{optional(String.t()) =&gt; [map()]}
```

Materializes all messages for an explicit backup operation.

# `stream_messages`
*optional* 

```elixir
@callback stream_messages(store :: t(), jid :: String.t(), opts :: keyword()) ::
  Enumerable.t()
```

Returns a lazy cursor over messages stored for one chat.

# `transaction`
*optional* 

```elixir
@callback transaction(store :: t(), (data() -&gt; data())) :: :ok | {:error, term()}
```

Atomically transforms all non-specialized top-level store data.

# `update`
*optional* 

```elixir
@callback update(store :: t(), key :: atom(), default :: term(), (term() -&gt; term())) ::
  :ok | {:error, term()}
```

Atomically updates one top-level store domain.

# `update_message_status`
*optional* 

```elixir
@callback update_message_status(
  store :: t(),
  jid :: String.t(),
  message_id :: String.t(),
  status :: atom()
) ::
  :ok | {:error, term()}
```

Updates the status of one message in a chat.

# `update_message_status_by_id`
*optional* 

```elixir
@callback update_message_status_by_id(
  store :: t(),
  message_id :: String.t(),
  status :: atom()
) ::
  :ok | {:error, term()}
```

Updates the status of every message matching an ID.

# `upsert_messages`
*optional* 

```elixir
@callback upsert_messages(store :: t(), jid :: String.t(), messages :: [map()]) ::
  :ok | {:error, term()}
```

Upserts messages without embedding them in chat metadata.

# `batch`

```elixir
@spec batch(t(), term(), (-&gt; term())) :: term()
```

Runs a function inside a caller-owned persistence batch.

# `begin_batch`

```elixir
@spec begin_batch(t(), term()) :: :ok | {:error, term()}
```

Starts a caller-owned write batch when supported by the adapter.

# `clear`

```elixir
@spec clear(t()) :: :ok | {:error, term()}
```

Clears all data from the store.

Only available if the adapter implements the optional `clear/1` callback.

# `close`

```elixir
@spec close(t()) :: :ok | {:error, term()}
```

Releases resources held by a store adapter when it implements `close/1`.

Adapters without process or connection resources need no callback.

# `delete_chat_messages`

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

Deletes all messages associated with one chat.

# `delete_messages`

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

Deletes selected messages after the caller has persisted them durably.

# `find_message`

```elixir
@spec find_message(t(), String.t(), String.t()) :: map() | nil
```

Finds one stored message in a chat.

# `find_message_by_id`

```elixir
@spec find_message_by_id(t(), String.t()) :: {String.t(), map()} | nil
```

Finds one stored message by ID across all chats.

# `finish_batch`

```elixir
@spec finish_batch(t(), term()) :: :ok | {:error, term()}
```

Finishes a caller-owned write batch and flushes it when supported.

# `flush`

```elixir
@spec flush(t()) :: :ok | {:error, term()}
```

Durably flushes pending adapter writes when the adapter supports it.

# `get`

```elixir
@spec get(t(), atom(), term()) :: term()
```

Reads a single top-level key from the store.

Adapters may override this with a more efficient implementation that avoids
loading and merging the entire persisted store.

# `get_and_update`

```elixir
@spec get_and_update(t(), atom(), term(), (term() -&gt; {term(), term()})) ::
  term() | {:error, term()}
```

Atomically updates one top-level domain and returns the updater's reply.

# `list_messages`

```elixir
@spec list_messages(t(), String.t(), keyword()) :: [map()]
```

Materializes a single page from the message stream.

# `load`

```elixir
@spec load(t()) :: {:ok, data()} | {:error, term()}
```

Loads data from a store adapter.

## Examples

    iex> store = ExWapp.Store.Memory.new([])
    iex> ExWapp.Store.load(store)
    {:ok, %{}}

# `load!`

```elixir
@spec load!(t()) :: data()
```

Loads data from store, returning empty map on error.

This is a convenience function that never fails. Use `load/1` if you
need to handle errors explicitly.

# `load_device_cache`

```elixir
@spec load_device_cache(t()) :: map()
```

Loads the recent device cache snapshot.

# `load_signal_sessions`

```elixir
@spec load_signal_sessions(t()) :: term()
```

Loads Signal session state from the store.

ETS-backed adapters may return a lightweight session-store reference instead
of a plain map so callers can avoid keeping large session state on process
heaps.

# `missing_optional_callbacks`

```elixir
@spec missing_optional_callbacks(t()) :: [{atom(), arity()}]
```

Reports which optional callbacks an adapter does not implement.

Every message-level callback is optional and dispatched through
`function_exported?/3`, so an adapter that misspells one, or never wrote it,
silently falls through to a generic path with different semantics — no
compile warning, no runtime error, just behaviour that quietly diverges.
Naming the gap once at session start turns that into something a reader can
see. Returns the missing `{function, arity}` pairs, newest adapters usually
returning `[]`.

# `new`

```elixir
@spec new(nil | module() | {module(), keyword()} | t()) :: t()
```

Creates a store instance from various input formats.

## Examples

    # Use default adapter
    iex> ExWapp.Store.new(nil)
    %ExWapp.Store.Ets{...}

    # Use module name
    iex> ExWapp.Store.new(ExWapp.Store.Memory)
    %ExWapp.Store.Memory{...}

    # Use module with options
    iex> ExWapp.Store.new({ExWapp.Store.Ets, path: "/tmp/test.etf"})
    %ExWapp.Store.Ets{path: "/tmp/test.etf", ...}

    # Pass existing struct
    iex> store = ExWapp.Store.Memory.new([])
    iex> ExWapp.Store.new(store) == store
    true

# `persist`

```elixir
@spec persist(t(), data()) :: :ok | {:error, term()}
```

Persists data via the store adapter.

## Examples

    iex> store = ExWapp.Store.Memory.new([])
    iex> ExWapp.Store.persist(store, %{foo: "bar"})
    :ok
    iex> ExWapp.Store.load(store)
    {:ok, %{foo: "bar"}}

# `persist_device_cache`

```elixir
@spec persist_device_cache(t(), map()) :: :ok | {:error, term()}
```

Persists the recent device cache snapshot.

# `persist_signal_sessions`

```elixir
@spec persist_signal_sessions(t(), term()) :: :ok | {:error, term()}
```

Persists Signal session state back to the store.

# `put`

```elixir
@spec put(t(), atom(), term()) :: :ok | {:error, term()}
```

Stores one top-level domain atomically.

# `read`

```elixir
@spec read(t(), (data() -&gt; term())) :: term()
```

Reads the adapter's non-specialized data through its write owner when supported.

# `snapshot`

```elixir
@spec snapshot(t()) :: {:ok, data()} | {:error, term()}
```

Returns a complete local snapshot including separately stored messages.

This is intentionally eager and should only be used for explicit backups.
Normal consumers should use stream_messages/3.

# `stream_messages`

```elixir
@spec stream_messages(t(), String.t(), keyword()) :: Enumerable.t()
```

Builds a lazy message stream for a chat.

Built-in adapters keep message payloads outside chat metadata. Custom
adapters can implement stream_messages/3 with a database cursor; adapters
that do not implement it use a compatibility fallback over :chat_messages.

# `transaction`

```elixir
@spec transaction(t(), (data() -&gt; data())) :: :ok | {:error, term()}
```

Atomically transforms the adapter's non-specialized top-level data.

Prefer `update/4` for a single domain because it avoids materializing the
rest of the store.

# `update`

```elixir
@spec update(t(), atom(), term(), (term() -&gt; term())) :: :ok | {:error, term()}
```

Atomically updates one top-level domain without a load/modify/persist race.

Built-in adapters execute `updater` inside their write owner. Custom adapters
can implement `update/4`; the compatibility fallback cannot guarantee
serialization across callers.

# `update_message_status`

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

Updates one stored message status.

# `update_message_status_by_id`

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

Updates message status by ID across all chats.

# `upsert_messages`

```elixir
@spec upsert_messages(t(), String.t(), [map()]) :: :ok | {:error, term()}
```

Inserts or replaces messages by ID in the dedicated message store.

---

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