Phoenix.Presence behaviour (Phoenix v1.6.0-rc.0) View Source

Provides Presence tracking to processes and channels.

This behaviour provides presence features such as fetching presences for a given topic, as well as handling diffs of join and leave events as they occur in real-time. Using this module defines a supervisor and a module that implements the Phoenix.Tracker behaviour that uses Phoenix.PubSub to broadcast presence updates.

In case you want to use only a subset of the functionality provided by Phoenix.Presence, such as tracking processes but without broadcasting updates, we recommend that you look at the Phoenix.Tracker functionality from the phoenix_pubsub project.

Example Usage

Start by defining a presence module within your application which uses Phoenix.Presence and provide the :otp_app which holds your configuration, as well as the :pubsub_server.

defmodule MyAppWeb.Presence do
  use Phoenix.Presence,
    otp_app: :my_app,
    pubsub_server: MyApp.PubSub
end

The :pubsub_server must point to an existing pubsub server running in your application, which is included by default as MyApp.PubSub for new applications.

Next, add the new supervisor to your supervision tree in lib/my_app/application.ex. It must be after the PubSub child and before the endpoint:

children = [
  ...
  {Phoenix.PubSub, name: MyApp.PubSub},
  MyAppWeb.Presence,
  MyAppWeb.Endpoint
]

Once added, presences can be tracked in your channel after joining:

defmodule MyAppWeb.MyChannel do
  use MyAppWeb, :channel
  alias MyAppWeb.Presence

  def join("some:topic", _params, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :user_id, ...)}
  end

  def handle_info(:after_join, socket) do
    {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
      online_at: inspect(System.system_time(:second))
    })

    push(socket, "presence_state", Presence.list(socket))
    {:noreply, socket}
  end
end

In the example above, Presence.track is used to register this channel's process as a presence for the socket's user ID, with a map of metadata. Next, the current presence information for the socket's topic is pushed to the client as a "presence_state" event.

Finally, a diff of presence join and leave events will be sent to the client as they happen in real-time with the "presence_diff" event. The diff structure will be a map of :joins and :leaves of the form:

%{
  joins: %{"123" => %{metas: [%{status: "away", phx_ref: ...}]}},
  leaves: %{"456" => %{metas: [%{status: "online", phx_ref: ...}]}}
},

See list/1 for more information on the presence data structure.

Fetching Presence Information

Presence metadata should be minimized and used to store small, ephemeral state, such as a user's "online" or "away" status. More detailed information, such as user details that need to be fetched from the database, can be achieved by overriding the fetch/2 function.

The fetch/2 callback is triggered when using list/1 and on every update, and it serves as a mechanism to fetch presence information a single time, before broadcasting the information to all channel subscribers. This prevents N query problems and gives you a single place to group isolated data fetching to extend presence metadata.

The function must return a map of data matching the outlined Presence data structure, including the :metas key, but can extend the map of information to include any additional information. For example:

def fetch(_topic, presences) do
  users = presences |> Map.keys() |> Accounts.get_users_map()

  for {key, %{metas: metas}} <- presences, into: %{} do
    {key, %{metas: metas, user: users[String.to_integer(key)]}}
  end
end

Where Account.get_users_map/1 could be implemented like:

def get_users_map(ids) do
  query =
    from u in User,
      where: u.id in ^ids,
      select: {u.id, u}

  query |> Repo.all() |> Enum.into(%{})
end

The fetch/2 function above fetches all users from the database who have registered presences for the given topic. The presences information is then extended with a :user key of the user's information, while maintaining the required :metas field from the original presence data.

Testing with Presence

Every time the fetch callback is invoked, it is done from a separate process. Given those processes run asynchronously, it is often necessary to guarantee they have been shutdown at the end of every test. This can be done by using ExUnit's on_exit hook plus fetchers_pids function:

on_exit(fn ->
  for pid <- MyAppWeb.Presence.fetchers_pids() do
    ref = Process.monitor(pid)
    assert_receive {:DOWN, ^ref, _, _, _}, 1000
  end
end)

Link to this section Summary

Callbacks

Extend presence information with additional data.

Returns the map of presence metadata for a socket/topic-key pair.

Returns presences for a socket/topic.

Track a channel's process as a presence.

Track an arbitrary process as a presence.

Stop tracking a channel's process.

Stop tracking a process.

Update a channel presence's metadata.

Update a process presence's metadata.

Link to this section Types

Specs

presence() :: %{key: String.t(), meta: map()}

Specs

presences() :: %{required(String.t()) => %{metas: [map()]}}

Specs

topic() :: String.t()

Link to this section Callbacks

Specs

fetch(topic(), presences()) :: presences()

Extend presence information with additional data.

When list/1 is used to list all presences of the given topic, this callback is triggered once to modify the result before it is broadcasted to all channel subscribers. This avoids N query problems and provides a single place to extend presence metadata. You must return a map of data matching the original result, including the :metas key, but can extend the map to include any additional information.

The default implementation simply passes presences through unchanged.

Example

def fetch(_topic, presences) do
  query =
    from u in User,
      where: u.id in ^Map.keys(presences),
      select: {u.id, u}

  users = query |> Repo.all() |> Enum.into(%{})
  for {key, %{metas: metas}} <- presences, into: %{} do
    {key, %{metas: metas, user: users[key]}}
  end
end

Specs

get_by_key(Phoenix.Socket.t() | topic(), key :: String.t()) :: presences()

Returns the map of presence metadata for a socket/topic-key pair.

Examples

Uses the same data format as list/1, but only returns metadata for the presences under a topic and key pair. For example, a user with key "user1", connected to the same chat room "room:1" from two devices, could return:

iex> MyPresence.get_by_key("room:1", "user1")
[%{name: "User 1", metas: [%{device: "Desktop"}, %{device: "Mobile"}]}]

Like list/1, the presence metadata is passed to the fetch callback of your presence module to fetch any additional information.

Specs

list(Phoenix.Socket.t() | topic()) :: presences()

Returns presences for a socket/topic.

Presence data structure

The presence information is returned as a map with presences grouped by key, cast as a string, and accumulated metadata, with the following form:

%{key => %{metas: [%{phx_ref: ..., ...}, ...]}}

For example, imagine a user with id 123 online from two different devices, as well as a user with id 456 online from just one device. The following presence information might be returned:

%{"123" => %{metas: [%{status: "away", phx_ref: ...},
                     %{status: "online", phx_ref: ...}]},
  "456" => %{metas: [%{status: "online", phx_ref: ...}]}}

The keys of the map will usually point to a resource ID. The value will contain a map with a :metas key containing a list of metadata for each resource. Additionally, every metadata entry will contain a :phx_ref key which can be used to uniquely identify metadata for a given key. In the event that the metadata was previously updated, a :phx_ref_prev key will be present containing the previous :phx_ref value.

Link to this callback

track(socket, key, meta)

View Source

Specs

track(socket :: Phoenix.Socket.t(), key :: String.t(), meta :: map()) ::
  {:ok, ref :: binary()} | {:error, reason :: term()}

Track a channel's process as a presence.

Tracked presences are grouped by key, cast as a string. For example, to group each user's channels together, use user IDs as keys. Each presence can be associated with a map of metadata to store small, ephemeral state, such as a user's online status. To store detailed information, see fetch/2.

Example

alias MyApp.Presence
def handle_info(:after_join, socket) do
  {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
    online_at: inspect(System.system_time(:second))
  })
  {:noreply, socket}
end
Link to this callback

track(pid, topic, key, meta)

View Source

Specs

track(pid(), topic(), key :: String.t(), meta :: map()) ::
  {:ok, ref :: binary()} | {:error, reason :: term()}

Track an arbitrary process as a presence.

Same with track/3, except track any process by topic and key.

Specs

untrack(socket :: Phoenix.Socket.t(), key :: String.t()) :: :ok

Stop tracking a channel's process.

Link to this callback

untrack(pid, topic, key)

View Source

Specs

untrack(pid(), topic(), key :: String.t()) :: :ok

Stop tracking a process.

Link to this callback

update(socket, key, meta)

View Source

Specs

update(
  socket :: Phoenix.Socket.t(),
  key :: String.t(),
  meta :: map() | (map() -> map())
) :: {:ok, ref :: binary()} | {:error, reason :: term()}

Update a channel presence's metadata.

Replace a presence's metadata by passing a new map or a function that takes the current map and returns a new one.

Link to this callback

update(pid, topic, key, meta)

View Source

Specs

update(pid(), topic(), key :: String.t(), meta :: map() | (map() -> map())) ::
  {:ok, ref :: binary()} | {:error, reason :: term()}

Update a process presence's metadata.

Same as update/3, but with an arbitrary process.