ExWapp.Store behaviour (ExWapp v0.1.2)

Copy Markdown View Source

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

Passing a Store to Session

# 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:

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

Summary

Callbacks

Begins a caller-owned persistence batch.

Optional callback to clear all stored data.

Optional callback that releases adapter-owned processes or connections.

Deletes every message associated with one chat.

Deletes selected messages after durable application persistence.

Finds one message in a chat by ID.

Finds one message by ID across all chats.

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

Durably flushes all pending writes without closing the adapter.

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

Loads persisted session data.

Creates a new store instance.

Persists session data.

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

Materializes all messages for an explicit backup operation.

Returns a lazy cursor over messages stored for one chat.

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

Atomically updates one top-level store domain.

Updates the status of one message in a chat.

Updates the status of every message matching an ID.

Upserts messages without embedding them in chat metadata.

Functions

Runs a function inside a caller-owned persistence batch.

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

Clears all data from the store.

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

Deletes all messages associated with one chat.

Deletes selected messages after the caller has persisted them durably.

Finds one stored message in a chat.

Finds one stored message by ID across all chats.

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

Durably flushes pending adapter writes when the adapter supports it.

Reads a single top-level key from the store.

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

Materializes a single page from the message stream.

Loads data from a store adapter.

Loads data from store, returning empty map on error.

Loads the recent device cache snapshot.

Loads Signal session state from the store.

Reports which optional callbacks an adapter does not implement.

Creates a store instance from various input formats.

Persists data via the store adapter.

Persists the recent device cache snapshot.

Persists Signal session state back to the store.

Stores one top-level domain atomically.

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

Returns a complete local snapshot including separately stored messages.

Builds a lazy message stream for a chat.

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

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

Updates one stored message status.

Updates message status by ID across all chats.

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

Types

data()

@type data() :: map()

t()

@type t() :: struct()

Callbacks

begin_batch(store, reason)

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

Begins a caller-owned persistence batch.

clear(store)

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

Optional callback to clear all stored data.

Useful for testing or when unlinking a device.

close(store)

(optional)
@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(store, jid)

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

Deletes every message associated with one chat.

delete_messages(store, jid, message_ids)

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

Deletes selected messages after durable application persistence.

find_message(store, jid, message_id)

(optional)
@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(store, message_id)

(optional)
@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(store, reason)

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

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

flush(store)

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

Durably flushes all pending writes without closing the adapter.

get_and_update(store, key, default, function)

(optional)
@callback get_and_update(
  store :: t(),
  key :: atom(),
  default :: term(),
  (term() -> {term(), term()})
) :: term() | {:error, term()}

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

load(store)

@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(opts)

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

Creates a new store instance.

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

persist(store, data)

@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(store, function)

(optional)
@callback read(store :: t(), (data() -> term())) :: term()

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

snapshot_messages(store)

(optional)
@callback snapshot_messages(store :: t()) :: %{optional(String.t()) => [map()]}

Materializes all messages for an explicit backup operation.

stream_messages(store, jid, opts)

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

Returns a lazy cursor over messages stored for one chat.

transaction(store, function)

(optional)
@callback transaction(store :: t(), (data() -> data())) :: :ok | {:error, term()}

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

update(store, key, default, function)

(optional)
@callback update(store :: t(), key :: atom(), default :: term(), (term() -> term())) ::
  :ok | {:error, term()}

Atomically updates one top-level store domain.

update_message_status(store, jid, message_id, status)

(optional)
@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(store, message_id, status)

(optional)
@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(store, jid, messages)

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

Upserts messages without embedding them in chat metadata.

Functions

batch(store, reason, fun)

@spec batch(t(), term(), (-> term())) :: term()

Runs a function inside a caller-owned persistence batch.

begin_batch(store, reason)

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

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

clear(store)

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

Clears all data from the store.

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

close(store)

@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(store, jid)

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

Deletes all messages associated with one chat.

delete_messages(store, jid, message_ids)

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

Deletes selected messages after the caller has persisted them durably.

find_message(store, jid, message_id)

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

Finds one stored message in a chat.

find_message_by_id(store, message_id)

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

Finds one stored message by ID across all chats.

finish_batch(store, reason)

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

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

flush(store)

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

Durably flushes pending adapter writes when the adapter supports it.

get(store, key, default \\ nil)

@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(store, key, default, updater)

@spec get_and_update(t(), atom(), term(), (term() -> {term(), term()})) ::
  term() | {:error, term()}

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

list_messages(store, jid, opts \\ [])

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

Materializes a single page from the message stream.

load(store)

@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!(store)

@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(store)

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

Loads the recent device cache snapshot.

load_signal_sessions(store)

@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(arg1)

@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(struct)

@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(store, data)

@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(store, cache)

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

Persists the recent device cache snapshot.

persist_signal_sessions(store, sessions)

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

Persists Signal session state back to the store.

put(store, key, value)

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

Stores one top-level domain atomically.

read(store, reader)

@spec read(t(), (data() -> term())) :: term()

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

snapshot(store)

@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(store, jid, opts \\ [])

@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(store, updater)

@spec transaction(t(), (data() -> 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(store, key, default, updater)

@spec update(t(), atom(), term(), (term() -> 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(store, jid, message_id, status)

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

Updates one stored message status.

update_message_status_by_id(store, message_id, status)

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

Updates message status by ID across all chats.

upsert_messages(store, jid, messages)

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

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