Espex.ConnectionListener behaviour (espex v0.5.0)

Copy Markdown View Source

Behaviour for being notified when the set of connected native-API clients changes.

Espex stays framework-agnostic — it has no Phoenix.PubSub dependency. Instead, configure a :connection_listener module (like the other adapters) and Espex calls connections_changed/0 once when a client finishes connecting (sends its HelloRequest) and once when a client disconnects. Per-frame activity updates do not trigger it.

The callback carries no payload by design: it is a lightweight "the set changed, go look" signal. On receipt, call Espex.connected_clients/1 to read the current set.

The callback is best-effort — reconcile on boot

connections_changed/0 is fire-and-forget. Espex invokes it in a detached process and catches every failure kind, so a slow, blocking, or crashing listener can never stall or drop a live client connection; it does not retry or buffer, and notifications are unordered. A client can connect the instant the TCP listener is up — possibly before your listener process is ready — so a notification can be missed during startup.

Because Espex.connected_clients/1 always reflects the live Registry, the robust pattern is treat the callback as a hint and the query as the truth: read connected_clients/1 once when your listener starts, then again on every connections_changed/0. That reconciles any notification missed before you were ready.

Start your listener before the Espex child (and use :rest_for_one) so it is ready as early as possible:

children = [
  {MyApp.ClientTracker, []},
  {Espex,
   server_name: MyApp.EspexServer,
   device_config: [name: "my-device"],
   connection_listener: MyApp.ClientTracker}
]

Supervisor.start_link(children, strategy: :rest_for_one)

Example

defmodule MyApp.ClientTracker do
  @behaviour Espex.ConnectionListener
  use GenServer

  def start_link(_), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)

  # --- ConnectionListener callback ---

  @impl Espex.ConnectionListener
  def connections_changed do
    # Cast (never block the connection process) and reconcile from
    # the source of truth.
    GenServer.cast(__MODULE__, :refresh)
  end

  # --- GenServer ---

  @impl GenServer
  def init(_) do
    # Reconcile on boot — catches any client that connected before
    # this process was ready.
    {:ok, %{clients: Espex.connected_clients(MyApp.EspexServer)}}
  end

  @impl GenServer
  def handle_cast(:refresh, state) do
    clients = Espex.connected_clients(MyApp.EspexServer)
    Phoenix.PubSub.broadcast(MyApp.PubSub, "espex:clients", {:clients, clients})
    {:noreply, %{state | clients: clients}}
  end
end

Summary

Callbacks

Invoked once when a client completes its hello (connect) and once when a client disconnects. Return value is ignored.

Callbacks

connections_changed()

@callback connections_changed() :: any()

Invoked once when a client completes its hello (connect) and once when a client disconnects. Return value is ignored.

Keep it fast and non-raising — cast to your own process and read Espex.connected_clients/1 there.