Packages
WhatsApp Web API client for Elixir. Full-featured port of Baileys with end-to-end Signal Protocol encryption, multi-device support, groups, communities, media, newsletters, and native BEAM fault tolerance.
Current section
Files
Jump to
Current section
Files
lib/baileys_ex/message/offline_queue.ex
defmodule BaileysEx.Message.OfflineQueue do
@moduledoc """
FIFO offline node batching owned by the caller's process state.
This mirrors Baileys' offline queue semantics without introducing a separate
process: the owner keeps the queue in its state, drains up to 10 nodes per
pass, and lets the caller reschedule the next drain when work remains.
"""
alias BaileysEx.BinaryNode
alias BaileysEx.Connection.EventEmitter
@valid_types [:message, :call, :receipt, :notification]
defstruct queue: :queue.new(), batch_size: 10, buffering?: false
@type node_type :: :message | :call | :receipt | :notification
@type t :: %__MODULE__{
queue: term(),
batch_size: pos_integer(),
buffering?: boolean()
}
@type drain_result :: %{
processed_count: non_neg_integer(),
continue?: boolean()
}
@doc """
Initializes a new offline queue.
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
%__MODULE__{batch_size: Keyword.get(opts, :batch_size, 10)}
end
@doc """
Pushes a new node onto the offline processing queue.
"""
@spec enqueue(t(), node_type(), BinaryNode.t()) :: t()
def enqueue(%__MODULE__{} = state, type, %BinaryNode{} = node) when type in @valid_types do
%{state | queue: :queue.in({type, node}, state.queue)}
end
@doc """
Drains up to the configured batch size of nodes from the queue.
"""
@spec drain(t(), map(), (node_type(), BinaryNode.t() -> :ok | {:error, term()})) ::
{:ok, t(), drain_result()} | {:error, term(), t()}
def drain(%__MODULE__{} = state, context, processor) when is_function(processor, 2) do
state = maybe_begin_buffering(state, context)
case drain_batch(state, processor, 0) do
{:ok, state, processed_count} ->
continue? = not :queue.is_empty(state.queue)
state = maybe_finish_buffering(state, context, continue?)
{:ok, state, %{processed_count: processed_count, continue?: continue?}}
{:error, reason, state, processed_count} ->
state = maybe_finish_buffering(state, context, false)
{:error, {:drain_failed, reason, processed_count}, state}
end
end
defp drain_batch(%__MODULE__{batch_size: batch_size} = state, _processor, processed_count)
when processed_count >= batch_size do
{:ok, state, processed_count}
end
defp drain_batch(%__MODULE__{} = state, processor, processed_count) do
if :queue.is_empty(state.queue) do
{:ok, state, processed_count}
else
{{:value, {type, node}}, queue} = :queue.out(state.queue)
state = %{state | queue: queue}
case processor.(type, node) do
:ok ->
drain_batch(state, processor, processed_count + 1)
{:error, reason} ->
{:error, reason, state, processed_count}
end
end
end
defp maybe_begin_buffering(%__MODULE__{buffering?: true} = state, _context), do: state
defp maybe_begin_buffering(%__MODULE__{} = state, %{event_emitter: event_emitter}) do
if not :queue.is_empty(state.queue) and not is_nil(event_emitter) do
:ok = EventEmitter.buffer(event_emitter)
%{state | buffering?: true}
else
state
end
end
defp maybe_begin_buffering(%__MODULE__{} = state, _context), do: state
defp maybe_finish_buffering(%__MODULE__{buffering?: false} = state, _context, _continue?),
do: state
defp maybe_finish_buffering(%__MODULE__{} = state, %{event_emitter: event_emitter}, true)
when not is_nil(event_emitter),
do: state
defp maybe_finish_buffering(%__MODULE__{} = state, %{event_emitter: event_emitter}, false)
when not is_nil(event_emitter) do
_ = EventEmitter.flush(event_emitter)
%{state | buffering?: false}
end
defp maybe_finish_buffering(%__MODULE__{} = state, _context, _continue?), do: state
end