Current section

Files

Jump to
amarula lib amarula protocol socket iq.ex
Raw

lib/amarula/protocol/socket/iq.ex

defmodule Amarula.Protocol.Socket.IQ do
@moduledoc """
IQ request/response correlation, extracted from `Connection` as a pure
module over the pending-IQ map. CM stays the only process and owns the socket,
timers, and the tracked-kind continuations; this module decides *what to do*
with a reply/timeout and returns an **effect** for CM to perform.
An outbound IQ is registered with one of two intents, keyed by its id:
* `{:tracked, kind, timer}` — an internal bootstrap step; on reply CM runs the
continuation for `kind` (login/prekey/digest/app-state/…).
* `{:waiter, from, timer}` / `{:waiter, from, timer, transform}` — a caller
blocked in `query_iq`; on reply CM `GenServer.reply`s (optionally mapping
the result through `transform`).
`resolve/2` and `timeout/2` return `{new_pending, effect}` where effect is:
* `{:reply, from, result}` — `GenServer.reply(from, result)`
* `{:reply, from, transform.(result)}` (already applied)
* `{:tracked, kind, result, timer}` — cancel `timer`, run the continuation
* `{:cancel_and_reply, timer, from, result}` — cancel `timer`, then reply
* `:none` — nothing to do (no waiter for this id)
Timer cancellation is surfaced as part of the effect so this module performs no
side effects.
"""
alias Amarula.Protocol.Binary.NodeUtils
@type kind :: atom()
@type entry ::
{:tracked, kind(), reference()}
| {:waiter, GenServer.from(), reference()}
| {:waiter, GenServer.from(), reference(), (term() -> term())}
@type pending :: %{optional(String.t()) => entry()}
@type effect ::
{:reply, GenServer.from(), term(), reference()}
| {:tracked, kind(), term(), reference()}
| :none
@doc "Register a tracked IQ (internal bootstrap step) by id."
@spec track(pending(), String.t(), kind(), reference()) :: pending()
def track(pending, id, kind, timer), do: Map.put(pending, id, {:tracked, kind, timer})
@doc "Register a blocking waiter (query_iq), optionally with a result transform."
@spec wait(pending(), String.t(), GenServer.from(), reference(), (term() -> term()) | nil) ::
pending()
def wait(pending, id, from, timer, nil), do: Map.put(pending, id, {:waiter, from, timer})
def wait(pending, id, from, timer, transform),
do: Map.put(pending, id, {:waiter, from, timer, transform})
@doc """
Resolve an incoming IQ reply `node`. Returns `{pending, effect}`. The result is
`{:ok, node}` for type=result, else `{:error, node}`.
"""
@spec resolve(pending(), Amarula.Protocol.Binary.Node.t()) :: {pending(), effect()}
def resolve(pending, node) do
id = NodeUtils.get_attr(node, "id")
result =
if NodeUtils.get_attr(node, "type") == "result", do: {:ok, node}, else: {:error, node}
dispatch(Map.pop(pending, id), result)
end
@doc "Resolve an IQ timeout for `id`. Returns `{pending, effect}`."
@spec timeout(pending(), String.t()) :: {pending(), effect()}
def timeout(pending, id) do
dispatch(Map.pop(pending, id), {:error, :timeout})
end
# --- pop → effect ---
defp dispatch({nil, pending}, _result), do: {pending, :none}
defp dispatch({{:waiter, from, timer}, pending}, result),
do: {pending, {:reply, from, result, timer}}
defp dispatch({{:waiter, from, timer, transform}, pending}, result),
do: {pending, {:reply, from, transform.(result), timer}}
defp dispatch({{:tracked, kind, timer}, pending}, result),
do: {pending, {:tracked, kind, result, timer}}
end